From d3b74548d1d45dcdd49d282d47828237b544c976 Mon Sep 17 00:00:00 2001 From: kl3inIT Date: Tue, 21 Jul 2026 17:08:17 +0700 Subject: [PATCH 1/2] refactor(web): reset frontend to production shell --- docs/guidelines/frontend-design-system.md | 21 + .../plan.md | 8 +- web/.oxlintrc.json | 17 + web/index.html | 4 +- web/package.json | 11 +- web/pnpm-lock.yaml | 402 +++++++++----- web/src/App.tsx | 3 - web/src/components/app-shell.tsx | 185 +++---- web/src/components/auth-gate.tsx | 41 +- web/src/components/layout/page-title.tsx | 8 - web/src/components/layout/topbar.tsx | 110 ---- web/src/components/mode-toggle.tsx | 4 +- .../components/states/application-error.tsx | 40 ++ web/src/components/states/page-loading.tsx | 14 + web/src/components/ui/button.tsx | 4 +- web/src/features/assets/asset-type-specs.ts | 375 ------------- web/src/features/assets/asset-type.ts | 35 -- web/src/features/assets/demo-data.ts | 58 -- web/src/features/assets/status-badge.tsx | 10 - web/src/features/assets/use-assets.ts | 95 ---- web/src/features/assets/workflow-diagram.tsx | 59 -- .../organization/use-organization-context.ts | 44 -- web/src/index.css | 3 +- web/src/lib/api.ts | 284 ---------- web/src/lib/query-client.ts | 17 + web/src/main.tsx | 70 ++- web/src/pages/analytics.tsx | 5 - web/src/pages/ask-memory.tsx | 199 ------- web/src/pages/asset-detail.tsx | 317 ----------- web/src/pages/create-asset.tsx | 341 ------------ web/src/pages/dashboard.tsx | 274 ---------- web/src/pages/knowledge-graph.tsx | 509 ------------------ web/src/pages/knowledge-transfer.tsx | 229 -------- web/src/pages/login.tsx | 26 +- web/src/pages/registry.tsx | 263 --------- web/src/pages/review-queue.tsx | 200 ------- web/src/pages/settings.tsx | 30 -- web/src/pages/workspace.tsx | 10 + web/src/router.tsx | 173 +++--- web/src/types/cytoscape-fcose.d.ts | 6 - 40 files changed, 631 insertions(+), 3873 deletions(-) create mode 100644 web/.oxlintrc.json delete mode 100644 web/src/App.tsx delete mode 100644 web/src/components/layout/page-title.tsx delete mode 100644 web/src/components/layout/topbar.tsx create mode 100644 web/src/components/states/application-error.tsx create mode 100644 web/src/components/states/page-loading.tsx delete mode 100644 web/src/features/assets/asset-type-specs.ts delete mode 100644 web/src/features/assets/asset-type.ts delete mode 100644 web/src/features/assets/demo-data.ts delete mode 100644 web/src/features/assets/status-badge.tsx delete mode 100644 web/src/features/assets/use-assets.ts delete mode 100644 web/src/features/assets/workflow-diagram.tsx delete mode 100644 web/src/features/organization/use-organization-context.ts delete mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/query-client.ts delete mode 100644 web/src/pages/analytics.tsx delete mode 100644 web/src/pages/ask-memory.tsx delete mode 100644 web/src/pages/asset-detail.tsx delete mode 100644 web/src/pages/create-asset.tsx delete mode 100644 web/src/pages/dashboard.tsx delete mode 100644 web/src/pages/knowledge-graph.tsx delete mode 100644 web/src/pages/knowledge-transfer.tsx delete mode 100644 web/src/pages/registry.tsx delete mode 100644 web/src/pages/review-queue.tsx delete mode 100644 web/src/pages/settings.tsx create mode 100644 web/src/pages/workspace.tsx delete mode 100644 web/src/types/cytoscape-fcose.d.ts diff --git a/docs/guidelines/frontend-design-system.md b/docs/guidelines/frontend-design-system.md index ebd84a7f..9a349ced 100644 --- a/docs/guidelines/frontend-design-system.md +++ b/docs/guidelines/frontend-design-system.md @@ -16,3 +16,24 @@ Required qualities: Do not copy old page layouts merely to preserve route parity. Reuse old code only when it is generic, tested, and compatible with the new information architecture. + +## Foundation Boundary + +- `main.tsx` owns process-level providers and the final React crash boundary. +- TanStack Router owns route pending, route error, not-found, typed search, and + route-level code splitting. +- TanStack Query owns server state. Initial failures render in context; only + failed background refreshes produce a global toast. +- The browser session is the authenticated-shell gate. Product routes must not + render before the session is verified. +- Ordinary REST clients are generated from root `contracts/openapi.json` with + Hey API. AI streaming remains a separate transport boundary. +- Raw exception text is development-only. Production states use safe messages + and an explicit retry path. + +## Retained Building Blocks + +Keep shadcn/ui registry primitives and the AI Elements foundation even while a +screen is not implemented. They are local product building blocks, not evidence +that the corresponding product feature already exists. Add product routes one +vertical slice at a time; do not restore the deleted prototype pages. diff --git a/docs/increments/active/2026-07-20-secure-knowledge-vertical-slice/plan.md b/docs/increments/active/2026-07-20-secure-knowledge-vertical-slice/plan.md index 4aeaffa6..642bb517 100644 --- a/docs/increments/active/2026-07-20-secure-knowledge-vertical-slice/plan.md +++ b/docs/increments/active/2026-07-20-secure-knowledge-vertical-slice/plan.md @@ -61,10 +61,12 @@ ## 7 — Minimal New Web Flow -- [ ] Add new shell and semantic light/dark tokens. -- [ ] Export OpenAPI and generate typed fetch/Zod/TanStack clients; keep the AI +- [x] Add new shell and semantic light/dark tokens. +- [x] Export OpenAPI and generate typed fetch/Zod/TanStack clients; keep the AI streaming transport separate from ordinary REST contracts. -- [ ] Add oxlint, Vitest, and one critical Playwright project before expanding UI. +- [x] Add oxlint with React/TypeScript correctness rules and generated/registry + exclusions. +- [ ] Add Vitest and one critical Playwright project before expanding UI. - [ ] Build Ask with visible waiting/tool/evidence/citation/error states. - [ ] Build Sources upload/status/privacy view and Review publication view. - [ ] Run real-browser two-user upload, answer, deny, and revoke flow. diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json new file mode 100644 index 00000000..eb7491f7 --- /dev/null +++ b/web/.oxlintrc.json @@ -0,0 +1,17 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "ignorePatterns": ["dist", "src/components/ui", "src/components/ai-elements", "src/lib/hey-api"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + }, + "overrides": [ + { + "files": ["src/main.tsx", "src/router.tsx"], + "rules": { + "react/only-export-components": "off" + } + } + ] +} diff --git a/web/index.html b/web/index.html index 2fc35950..f0471a38 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,9 @@ - + + + OrgMemory diff --git a/web/package.json b/web/package.json index 98e35230..5a8075c8 100644 --- a/web/package.json +++ b/web/package.json @@ -6,32 +6,31 @@ "scripts": { "dev": "vite", "gen:api": "corepack pnpm dlx --package @hey-api/openapi-ts@0.99.0 --package typescript@6.0.1-rc openapi-ts -i ../contracts/openapi.json -o src/lib/hey-api -c @hey-api/client-fetch -p @hey-api/typescript @hey-api/sdk @tanstack/react-query zod", - "build": "corepack pnpm gen:api && tsc -b && vite build", + "build": "corepack pnpm gen:api && corepack pnpm lint && tsc -b && vite build", + "lint": "oxlint", "typecheck": "corepack pnpm gen:api && tsc -b", "preview": "vite preview" }, "dependencies": { "@ai-sdk/react": "^4.0.34", + "@radix-ui/react-slot": "^1.3.0", "@streamdown/cjk": "^1.0.3", "@streamdown/code": "^1.1.1", "@streamdown/math": "^1.0.2", "@streamdown/mermaid": "^1.0.2", "@tanstack/react-query": "^5.101.2", - "@tanstack/react-query-devtools": "^5.101.2", "@tanstack/react-router": "^1.170.18", - "@xyflow/react": "^12.11.2", "ai": "^7.0.31", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "cytoscape": "^3.34.0", - "cytoscape-fcose": "^2.2.0", "lucide-react": "^1.25.0", "nanoid": "^6.0.0", "next-themes": "^0.4.6", "radix-ui": "^1.6.3", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-error-boundary": "^6.1.2", "recharts": "^3.9.2", "sonner": "^2.0.7", "streamdown": "^2.5.0", @@ -41,10 +40,12 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query-devtools": "^5.101.2", "@types/node": "^26.1.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.73.0", "tailwindcss": "^4.3.3", "tw-animate-css": "^1.4.0", "typescript": "~7.0.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 7d8f18ca..0b6ef395 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ai-sdk/react': specifier: ^4.0.34 version: 4.0.34(react@19.2.7)(zod@4.4.3) + '@radix-ui/react-slot': + specifier: ^1.3.0 + version: 1.3.0(@types/react@19.2.17)(react@19.2.7) '@streamdown/cjk': specifier: ^1.0.3 version: 1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7)(unified@11.0.5) @@ -26,15 +29,9 @@ importers: '@tanstack/react-query': specifier: ^5.101.2 version: 5.101.2(react@19.2.7) - '@tanstack/react-query-devtools': - specifier: ^5.101.2 - version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7) '@tanstack/react-router': specifier: ^1.170.18 version: 1.170.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@xyflow/react': - specifier: ^12.11.2 - version: 12.11.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ai: specifier: ^7.0.31 version: 7.0.31(zod@4.4.3) @@ -47,12 +44,6 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - cytoscape: - specifier: ^3.34.0 - version: 3.34.0 - cytoscape-fcose: - specifier: ^2.2.0 - version: 2.2.0(cytoscape@3.34.0) lucide-react: specifier: ^1.25.0 version: 1.25.0(react@19.2.7) @@ -71,6 +62,9 @@ importers: react-dom: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) + react-error-boundary: + specifier: ^6.1.2 + version: 6.1.2(react@19.2.7) recharts: specifier: ^3.9.2 version: 3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) @@ -93,6 +87,9 @@ importers: '@tailwindcss/vite': specifier: ^4.3.3 version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) + '@tanstack/react-query-devtools': + specifier: ^5.101.2 + version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7) '@types/node': specifier: ^26.1.1 version: 26.1.1 @@ -105,6 +102,9 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.3 version: 6.0.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) + oxlint: + specifier: ^1.73.0 + version: 1.73.0 tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -184,8 +184,8 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.1.3': - resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -215,6 +215,128 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxlint/binding-android-arm-eabi@1.73.0': + resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.73.0': + resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.73.0': + resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.73.0': + resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.73.0': + resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.73.0': + resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.73.0': + resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.73.0': + resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.73.0': + resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.73.0': + resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -1390,8 +1512,8 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} @@ -1545,8 +1667,8 @@ packages: cpu: [x64] os: [win32] - '@ungap/structured-clone@1.3.2': - resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} @@ -1571,22 +1693,6 @@ packages: '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} - '@xyflow/react@12.11.2': - resolution: {integrity: sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==} - peerDependencies: - '@types/react': '>=17' - '@types/react-dom': '>=17' - react: '>=17' - react-dom: '>=17' - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@xyflow/system@0.0.79': - resolution: {integrity: sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==} - ai@7.0.31: resolution: {integrity: sha512-pJfwKXjF5kw0rKRTePwYo60EfWb8wfzJAgf3ojln/YkOsVVKttzZAJVcRPsg37Z3a06ZdKkxX+DSrMAFlPm5Mw==} engines: {node: '>=22'} @@ -1618,9 +1724,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - classcat@5.0.5: - resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1845,8 +1948,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dompurify@3.4.11: - resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} enhanced-resolve@5.24.2: resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} @@ -2323,6 +2426,19 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + oxlint@1.73.0: + resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + package-manager-detector@1.7.0: resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} @@ -2377,6 +2493,11 @@ packages: peerDependencies: react: ^19.2.7 + react-error-boundary@6.1.2: + resolution: {integrity: sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-is@19.2.7: resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} @@ -2741,21 +2862,6 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2843,7 +2949,7 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@3.1.3': + '@iconify/utils@3.1.4': dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 @@ -2881,6 +2987,63 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@oxlint/binding-android-arm-eabi@1.73.0': + optional: true + + '@oxlint/binding-android-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-x64@1.73.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.73.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.73.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.73.0': + optional: true + '@radix-ui/number@1.1.2': {} '@radix-ui/primitive@1.1.4': {} @@ -3776,7 +3939,7 @@ snapshots: dependencies: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 '@shikijs/engine-javascript@3.23.0': @@ -3801,7 +3964,7 @@ snapshots: '@shikijs/types@3.23.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/vscode-textmate@10.0.2': {} @@ -4085,7 +4248,7 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/hast@3.0.4': + '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -4178,7 +4341,7 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@ungap/structured-clone@1.3.2': {} + '@ungap/structured-clone@1.3.3': {} '@upsetjs/venn.js@2.0.0': optionalDependencies: @@ -4194,31 +4357,6 @@ snapshots: '@workflow/serde@4.1.0': {} - '@xyflow/react@12.11.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@xyflow/system': 0.0.79 - classcat: 5.0.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - zustand: 4.5.7(@types/react@19.2.17)(immer@11.1.11)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - transitivePeerDependencies: - - immer - - '@xyflow/system@0.0.79': - dependencies: - '@types/d3-drag': 3.0.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - ai@7.0.31(zod@4.4.3): dependencies: '@ai-sdk/gateway': 4.0.23(zod@4.4.3) @@ -4246,8 +4384,6 @@ snapshots: dependencies: clsx: 2.1.1 - classcat@5.0.5: {} - clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): @@ -4490,7 +4626,7 @@ snapshots: dependencies: dequal: 2.0.3 - dompurify@3.4.11: + dompurify@3.4.12: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -4530,20 +4666,20 @@ snapshots: hast-util-from-dom@5.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript: 9.0.1 web-namespaces: 2.0.1 hast-util-from-html-isomorphic@2.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-from-dom: 5.0.1 hast-util-from-html: 2.0.3 unist-util-remove-position: 5.0.0 hast-util-from-html@2.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 devlop: 1.1.0 hast-util-from-parse5: 8.0.3 parse5: 7.3.0 @@ -4552,7 +4688,7 @@ snapshots: hast-util-from-parse5@8.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 9.0.1 @@ -4563,17 +4699,17 @@ snapshots: hast-util-is-element@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-parse-selector@4.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw@9.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.2 + '@ungap/structured-clone': 1.3.3 hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.1 html-void-elements: 3.0.0 @@ -4587,13 +4723,13 @@ snapshots: hast-util-sanitize@5.0.2: dependencies: - '@types/hast': 3.0.4 - '@ungap/structured-clone': 1.3.2 + '@types/hast': 3.0.5 + '@ungap/structured-clone': 1.3.3 unist-util-position: 5.0.0 hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -4608,7 +4744,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 @@ -4627,7 +4763,7 @@ snapshots: hast-util-to-parse5@8.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 property-information: 7.2.0 @@ -4637,18 +4773,18 @@ snapshots: hast-util-to-text@4.0.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 hast-util-is-element: 3.0.0 unist-util-find-after: 5.0.0 hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 7.2.0 @@ -4851,7 +4987,7 @@ snapshots: mdast-util-math@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 longest-streak: 3.1.0 @@ -4864,7 +5000,7 @@ snapshots: mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -4875,7 +5011,7 @@ snapshots: mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 @@ -4892,7 +5028,7 @@ snapshots: mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -4907,9 +5043,9 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.2 + '@ungap/structured-clone': 1.3.3 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -4958,7 +5094,7 @@ snapshots: mermaid@11.16.0: dependencies: '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.3 + '@iconify/utils': 3.1.4 '@mermaid-js/parser': 1.2.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 @@ -4969,7 +5105,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.21 - dompurify: 3.4.11 + dompurify: 3.4.12 es-toolkit: 1.49.0 katex: 0.16.47 khroma: 2.1.0 @@ -5231,6 +5367,28 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + oxlint@1.73.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.73.0 + '@oxlint/binding-android-arm64': 1.73.0 + '@oxlint/binding-darwin-arm64': 1.73.0 + '@oxlint/binding-darwin-x64': 1.73.0 + '@oxlint/binding-freebsd-x64': 1.73.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 + '@oxlint/binding-linux-arm-musleabihf': 1.73.0 + '@oxlint/binding-linux-arm64-gnu': 1.73.0 + '@oxlint/binding-linux-arm64-musl': 1.73.0 + '@oxlint/binding-linux-ppc64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-musl': 1.73.0 + '@oxlint/binding-linux-s390x-gnu': 1.73.0 + '@oxlint/binding-linux-x64-gnu': 1.73.0 + '@oxlint/binding-linux-x64-musl': 1.73.0 + '@oxlint/binding-openharmony-arm64': 1.73.0 + '@oxlint/binding-win32-arm64-msvc': 1.73.0 + '@oxlint/binding-win32-ia32-msvc': 1.73.0 + '@oxlint/binding-win32-x64-msvc': 1.73.0 + package-manager-detector@1.7.0: {} parse-entities@4.0.2: @@ -5338,6 +5496,10 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-error-boundary@6.1.2(react@19.2.7): + dependencies: + react: 19.2.7 + react-is@19.2.7: {} react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): @@ -5420,7 +5582,7 @@ snapshots: rehype-katex@7.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/katex': 0.16.8 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 @@ -5430,13 +5592,13 @@ snapshots: rehype-raw@7.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw: 9.1.0 vfile: 6.0.3 rehype-sanitize@6.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-sanitize: 5.0.2 remark-cjk-friendly-gfm-strikethrough@2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): @@ -5493,7 +5655,7 @@ snapshots: remark-rehype@11.1.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.1 unified: 11.0.5 @@ -5560,7 +5722,7 @@ snapshots: '@shikijs/themes': 3.23.0 '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: @@ -5783,12 +5945,4 @@ snapshots: zod@4.4.3: {} - zustand@4.5.7(@types/react@19.2.17)(immer@11.1.11)(react@19.2.7): - dependencies: - use-sync-external-store: 1.6.0(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - immer: 11.1.11 - react: 19.2.7 - zwitch@2.0.4: {} diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index 716a15aa..00000000 --- a/web/src/App.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function App() { - return null -} diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx index f9b8450a..14ff5be4 100644 --- a/web/src/components/app-shell.tsx +++ b/web/src/components/app-shell.tsx @@ -1,120 +1,85 @@ -import { Link, Outlet, useLocation } from "@tanstack/react-router" -import { - BarChart3, - BookOpen, - Home, - Inbox, - Layers, - Network, - Settings, - Sparkles, - UserPlus, - type LucideIcon, -} from "lucide-react" -import { useState } from "react" +import { LogOut } from "lucide-react" +import { toast } from "sonner" + import { ModeToggle } from "@/components/mode-toggle" -import { Topbar } from "@/components/layout/topbar" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarProvider, - SidebarRail, - SidebarTrigger, -} from "@/components/ui/sidebar" -import { useAssets } from "@/features/assets/use-assets" + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { submitBrowserLogout } from "@/features/session/logout" +import { useBrowserSession } from "@/features/session/use-browser-session" -type RoutePath = "/" | "/registry" | "/create" | "/review" | "/transfer" | "/ask" | "/graph" | "/analytics" | "/settings" - -const NAV: Array<{ label: string; icon: LucideIcon; to: RoutePath; exact?: boolean }> = [ - { label: "Dashboard", icon: Home, to: "/", exact: true }, - { label: "Capability Registry", icon: BookOpen, to: "/registry" }, - { label: "Review Queue", icon: Inbox, to: "/review" }, - { label: "Onboarding", icon: UserPlus, to: "/transfer" }, - { label: "Analytics", icon: BarChart3, to: "/analytics" }, - { label: "Ask Memory", icon: Sparkles, to: "/ask" }, - { label: "Knowledge Graph", icon: Network, to: "/graph" }, - { label: "Settings", icon: Settings, to: "/settings" }, -] +function initials(name?: string, email?: string) { + const source = name?.trim() || email?.trim() || "OrgMemory" + return source + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]) + .join("") + .toUpperCase() +} export function AppShell() { - const pathname = useLocation({ select: (location) => location.pathname }) - const [query, setQuery] = useState("") - const { data } = useAssets() - const reviewCount = (data ?? []).filter((asset) => asset.status === "IN_REVIEW" || asset.status === "DRAFT").length + const session = useBrowserSession() + const identity = session.data - return ( - - - - - - - - OrgMemory - - + async function signOut() { + try { + await submitBrowserLogout() + } catch { + toast.error("Could not sign out. Try again.") + } + } - - - {NAV.map((item) => { - const isActive = item.exact ? pathname === item.to : pathname.startsWith(item.to) - return ( - - - - - {item.label} - - - {item.to === "/review" && reviewCount ? ( - - {reviewCount} - + return ( +
+ + Skip to content + +
+
+ OrgMemory +
+ + + + + + + +

{identity?.name || "Company account"}

+ {identity?.email ? ( +

{identity.email}

) : null} - - ) - })} - - - - - - - - - - New Asset - - - - - - - - - - - - - - -
- -
-
- +
+ + void signOut()}> + +
+
+
+
+
+
+
) } diff --git a/web/src/components/auth-gate.tsx b/web/src/components/auth-gate.tsx index d7f6c440..ab23d7eb 100644 --- a/web/src/components/auth-gate.tsx +++ b/web/src/components/auth-gate.tsx @@ -1,8 +1,7 @@ -import { Loader2, ShieldAlert } from "lucide-react" import { useEffect, type ReactNode } from "react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" + +import { ApplicationError } from "@/components/states/application-error" +import { PageLoading } from "@/components/states/page-loading" import { beginBrowserLogin, currentReturnPath } from "@/features/session/browser-login" import { useBrowserSession } from "@/features/session/use-browser-session" @@ -12,9 +11,7 @@ function AuthenticationRedirect() { }, []) return ( -
- -
+ ) } @@ -22,33 +19,17 @@ export function AuthGate({ children }: { children: ReactNode }) { const session = useBrowserSession() if (session.isPending) { - return ( -
-
- - - -
-
- ) + return } if (session.isError) { return ( -
- - - - Workspace access could not be verified - - The identity service may be unavailable, or this account has not been provisioned in OrgMemory. - - - - - - -
+ void session.refetch()} + /> ) } diff --git a/web/src/components/layout/page-title.tsx b/web/src/components/layout/page-title.tsx deleted file mode 100644 index 9c16da10..00000000 --- a/web/src/components/layout/page-title.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export function PageTitle({ title, subtitle }: { title: string; subtitle: string }) { - return ( -
-

{title}

-

{subtitle}

-
- ) -} diff --git a/web/src/components/layout/topbar.tsx b/web/src/components/layout/topbar.tsx deleted file mode 100644 index 162fd9bd..00000000 --- a/web/src/components/layout/topbar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useNavigate } from "@tanstack/react-router" -import { Bell, ChevronDown, HelpCircle, LogOut, Search } from "lucide-react" -import { toast } from "sonner" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { useOrganizationLookups, userInitials } from "@/features/organization/use-organization-context" -import { submitBrowserLogout } from "@/features/session/logout" -import { useBrowserSession } from "@/features/session/use-browser-session" - -export function Topbar({ query, onQueryChange }: { query: string; onQueryChange: (value: string) => void }) { - const navigate = useNavigate() - const { data: session } = useBrowserSession() - const { users } = useOrganizationLookups() - const fallbackUser = users.find((user) => user.role === "ADMIN") ?? users[0] - const displayName = session?.name ?? fallbackUser?.name ?? "OrgMemory User" - const displayDetail = session?.email ?? fallbackUser?.role.replace("_", " ") ?? "Authenticated user" - - function submitSearch() { - const trimmed = query.trim() - if (!trimmed) return - sessionStorage.setItem("orgmemory:registry-query", trimmed) - navigate({ to: "/registry" }) - } - - return ( -
-
Organizational AI Memory
-
-
- - onQueryChange(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - submitSearch() - } - }} - placeholder="Search assets, owners, teams..." - /> -
- - - - - - - - - {displayDetail} - - void submitBrowserLogout().catch(() => toast.error("Could not start sign out."))} - > - - Sign out - - - -
-
- ) -} diff --git a/web/src/components/mode-toggle.tsx b/web/src/components/mode-toggle.tsx index 089a78eb..5090e48b 100644 --- a/web/src/components/mode-toggle.tsx +++ b/web/src/components/mode-toggle.tsx @@ -3,8 +3,8 @@ import { useTheme } from "next-themes" import { Button } from "@/components/ui/button" export function ModeToggle() { - const { theme, setTheme } = useTheme() - const isDark = theme === "dark" + const { resolvedTheme, setTheme } = useTheme() + const isDark = resolvedTheme === "dark" return ( : null} + + + ) +} diff --git a/web/src/components/states/page-loading.tsx b/web/src/components/states/page-loading.tsx new file mode 100644 index 00000000..f3b2d4dd --- /dev/null +++ b/web/src/components/states/page-loading.tsx @@ -0,0 +1,14 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export function PageLoading({ label = "Loading workspace" }: { label?: string }) { + return ( +
+ + {label} +
+ ) +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index 4d38506c..eec52fba 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import { Slot } from "@radix-ui/react-slot" import { cn } from "@/lib/utils" @@ -48,7 +48,7 @@ function Button({ VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot : "button" return ( = { - PROMPT_TEMPLATE: { - type: "PROMPT_TEMPLATE", - departmentName: "Operations", - captureLabel: "Raw decision or writing pattern", - contentLabel: "Reusable Prompt Template", - description: "A reusable prompt with variables, tone rules, and output shape.", - requiredInputs: ["Source notes", "Audience", "Decision context"], - expectedOutputs: ["Draft artifact", "Assumptions", "Decision owner"], - rawCapture: - "We often turn rough operating notes into an executive decision memo. The prompt should compare options, state risks, recommend one path, and assign an owner.", - template: { - title: "Executive Decision Memo Prompt", - summary: "Turns rough operating notes into an executive-ready decision memo with options, risks, recommendation, and owner.", - assetType: "PROMPT_TEMPLATE", - useCase: "Executive decision support", - businessProcess: "Operations planning", - aiTool: "Claude", - tagNames: "prompt-template, decision-memo, leadership", - promptTemplate: - "Turn {{source_notes}} into an executive decision memo for {{audience}}. Include options, tradeoffs, recommendation, owner, and due date.", - workflowStepsJson: - '[{"name":"Collect source notes"},{"name":"Identify decision options"},{"name":"Compare tradeoffs"},{"name":"Draft recommendation"},{"name":"Assign owner"}]', - inputSchemaJson: '{"source_notes":"string","audience":"string","decision_context":"string","deadline":"string"}', - outputSchemaJson: '{"memo":"markdown","options":"array","recommendation":"string","owner":"string"}', - exampleInput: "Leadership notes about packaging changes and support cost risk.", - exampleOutput: "Decision memo with three options, tradeoffs, and recommended next action.", - riskLevel: "MEDIUM", - }, - }, - WORKFLOW_AUTOMATION: { - type: "WORKFLOW_AUTOMATION", - departmentName: "Customer Success", - captureLabel: "Raw repeatable workflow", - contentLabel: "Workflow Instructions", - description: "A multi-step process that transforms business inputs into a reviewed output.", - requiredInputs: ["Source data", "Time range", "Business unit"], - expectedOutputs: ["Workflow result", "Action list", "Review notes"], - rawCapture: - "Growth ops uses n8n to pull new demo requests from HubSpot, enrich company data, summarize intent with AI, route qualified leads to Slack, and create follow-up tasks.", - template: { - title: "Inbound Lead Enrichment Automation", - summary: "Enriches inbound demo requests, summarizes buying intent, routes qualified leads, and creates CRM follow-up tasks.", - assetType: "WORKFLOW_AUTOMATION", - useCase: "Inbound lead routing", - businessProcess: "Revenue operations", - aiTool: "n8n", - tagNames: "workflow, n8n, lead-enrichment, revops", - promptTemplate: - "Summarize {{lead_context}} and classify buying intent, segment, urgency, and recommended next action for the revenue team.", - workflowStepsJson: - '[{"name":"Trigger from HubSpot form"},{"name":"Enrich company profile"},{"name":"Classify buying intent"},{"name":"Route to Slack"},{"name":"Create CRM task"}]', - inputSchemaJson: '{"lead_context":"object","company_domain":"string","source_form":"string","routing_rules":"array"}', - outputSchemaJson: '{"intent":"string","segment":"string","urgency":"string","recommended_action":"string","crm_task":"object"}', - exampleInput: "Demo request from a 400-person SaaS company mentioning SOC2 and migration timeline.", - exampleOutput: "High-intent enterprise lead routed to AE with enriched account summary.", - riskLevel: "MEDIUM", - }, - }, - AI_AGENT: { - type: "AI_AGENT", - departmentName: "Sales", - captureLabel: "Agent operating brief", - contentLabel: "Agent Instructions", - description: "A task-oriented agent with goal, tools, decision rules, and escalation boundaries.", - requiredInputs: ["Goal", "Tools/data sources", "Escalation rules"], - expectedOutputs: ["Recommended action", "Reasoning summary", "Escalation path"], - rawCapture: - "Engineering wants a Claude Code/Codex agent recipe for small repository changes: read issue, inspect files, edit scoped code, run tests, and produce a reviewable summary.", - template: { - title: "Repository Change Agent Recipe", - summary: "Guides Claude Code or Codex through scoped repo changes with file inspection, patching, tests, and handoff summary.", - assetType: "AI_AGENT", - useCase: "AI-assisted software delivery", - businessProcess: "Engineering delivery", - aiTool: "Claude Code", - tagNames: "ai-agent, codex, claude-code, engineering", - promptTemplate: - "Act as a repository change agent. Use {{issue_context}}, inspect relevant files, make scoped edits, run {{verification_commands}}, and summarize risks.", - workflowStepsJson: - '[{"name":"Read issue and repo context"},{"name":"Inspect relevant files"},{"name":"Apply scoped patch"},{"name":"Run verification"},{"name":"Write handoff summary"}]', - inputSchemaJson: '{"issue_context":"string","repo_path":"string","verification_commands":"array","constraints":"string"}', - outputSchemaJson: '{"changed_files":"array","summary":"string","tests":"array","risks":"array"}', - exampleInput: "Bug report for a React table filter and repo path with pnpm typecheck command.", - exampleOutput: "Patch, passing checks, changed-files summary, and residual risks.", - riskLevel: "HIGH", - }, - }, - KNOWLEDGE_BOT: { - type: "KNOWLEDGE_BOT", - departmentName: "People Operations", - captureLabel: "Knowledge source and answer policy", - contentLabel: "Knowledge Bot Behavior", - description: "A source-grounded Q&A capability over approved internal knowledge.", - requiredInputs: ["Question", "Approved sources", "Escalation owner"], - expectedOutputs: ["Grounded answer", "Sources", "Confidence"], - rawCapture: - "Teams ask repeated questions about product limits, pricing exceptions, implementation SOPs, and internal policies. The bot should answer from approved sources and cite them.", - template: { - title: "Enterprise Knowledge RAG Assistant", - summary: "Answers internal questions from approved product, policy, pricing, and SOP sources with citations and escalation guidance.", - assetType: "KNOWLEDGE_BOT", - useCase: "Enterprise knowledge retrieval", - businessProcess: "Knowledge operations", - aiTool: "LlamaIndex", - tagNames: "knowledge-bot, rag, enterprise-knowledge", - promptTemplate: - "Answer {{question}} using only {{approved_sources}}. Cite sources, state confidence, and route uncertain cases to {{escalation_owner}}.", - workflowStepsJson: - '[{"name":"Receive question"},{"name":"Retrieve approved sources"},{"name":"Draft cited answer"},{"name":"Assess confidence"},{"name":"Escalate uncertain cases"}]', - inputSchemaJson: '{"question":"string","approved_sources":"array","escalation_owner":"string"}', - outputSchemaJson: '{"answer":"string","sources":"array","confidence":"string","escalation":"string"}', - exampleInput: "AE asks whether a customer can get an implementation exception.", - exampleOutput: "Cited answer from pricing policy and implementation SOP with confidence.", - riskLevel: "MEDIUM", - }, - }, - ANALYTICS_BRIEF: { - type: "ANALYTICS_BRIEF", - departmentName: "Product", - captureLabel: "Metrics and analysis cadence", - contentLabel: "Analytics Brief Instructions", - description: "A recurring analysis package that explains trends, anomalies, and decisions.", - requiredInputs: ["Metric export", "Period", "Segment"], - expectedOutputs: ["Executive summary", "Drivers", "Recommended decisions"], - rawCapture: - "Product teams need a monthly adoption brief from feature usage, expansion signals, churn risk, and qualitative notes.", - template: { - title: "Product Adoption Insights Brief", - summary: "Summarizes feature adoption, usage movement, churn signals, and recommended product follow-up decisions.", - assetType: "ANALYTICS_BRIEF", - useCase: "Product adoption reporting", - businessProcess: "Product analytics", - aiTool: "Claude", - tagNames: "analytics-brief, product, adoption", - promptTemplate: - "Create an adoption brief from {{usage_export}}, {{segment}}, and {{qualitative_notes}}. Explain trend drivers and recommended decisions.", - workflowStepsJson: - '[{"name":"Import usage data"},{"name":"Detect movement"},{"name":"Find drivers"},{"name":"Summarize risks"},{"name":"Recommend decisions"}]', - inputSchemaJson: '{"usage_export":"string","period":"string","segment":"string","qualitative_notes":"string"}', - outputSchemaJson: '{"brief":"markdown","drivers":"array","risks":"array","recommendations":"array"}', - exampleInput: "Feature usage export for admins in the last 30 days.", - exampleOutput: "Adoption brief with top drivers and recommended product actions.", - riskLevel: "MEDIUM", - }, - }, - CONTENT_GENERATOR: { - type: "CONTENT_GENERATOR", - departmentName: "Marketing", - captureLabel: "Content brief", - contentLabel: "Content Generation Instructions", - description: "A governed generator for business content, decks, emails, or knowledge articles.", - requiredInputs: ["Audience", "Source facts", "Tone constraints"], - expectedOutputs: ["Draft content", "Claims to review", "Variants"], - rawCapture: - "Marketing needs to turn a product launch brief into a board-ready slide deck, short demo video script, speaker notes, visual direction, and review checklist.", - template: { - title: "Launch Deck and Demo Video Generator", - summary: "Creates a slide deck outline, demo video script, speaker notes, visual direction, and claims review checklist from a launch brief.", - assetType: "CONTENT_GENERATOR", - useCase: "Launch content production", - businessProcess: "Product marketing", - aiTool: "Canva", - tagNames: "content-generator, slides, video, product-marketing", - promptTemplate: - "Create launch assets from {{launch_brief}} for {{audience}}: slide deck outline, demo video script, speaker notes, visual direction, and claims to review.", - workflowStepsJson: - '[{"name":"Capture launch brief"},{"name":"Draft narrative arc"},{"name":"Create slide outline"},{"name":"Write demo video script"},{"name":"Flag claims for review"}]', - inputSchemaJson: '{"launch_brief":"string","audience":"string","brand_constraints":"string","demo_flow":"string"}', - outputSchemaJson: '{"slides":"array","video_script":"string","speaker_notes":"array","visual_direction":"string","claims_to_review":"array"}', - exampleInput: "Launch brief for an AI governance feature aimed at enterprise ops leaders.", - exampleOutput: "10-slide launch outline, 90-second script, speaker notes, and visual direction.", - riskLevel: "MEDIUM", - }, - }, - DATA_EXTRACTION: { - type: "DATA_EXTRACTION", - departmentName: "Finance", - captureLabel: "Extraction source and target fields", - contentLabel: "Extraction Instructions", - description: "A structured extraction asset that turns messy documents into fields or records.", - requiredInputs: ["Document text", "Target schema", "Validation rules"], - expectedOutputs: ["Extracted fields", "Confidence", "Exceptions"], - rawCapture: - "Finance receives vendor contracts and needs to extract renewal date, termination window, payment terms, obligations, and unusual clauses.", - template: { - title: "Contract Obligation Extractor", - summary: "Extracts renewal dates, notice windows, payment terms, obligations, and clause exceptions from vendor contracts.", - assetType: "DATA_EXTRACTION", - useCase: "Contract metadata extraction", - businessProcess: "Vendor management", - aiTool: "OpenAI GPT-4o", - tagNames: "data-extraction, contracts, finance", - promptTemplate: - "Extract contract fields from {{contract_text}} using {{target_schema}}. Flag ambiguity, unusual clauses, and confidence for each field.", - workflowStepsJson: - '[{"name":"Import contract text"},{"name":"Extract target fields"},{"name":"Validate dates and amounts"},{"name":"Flag exceptions"},{"name":"Route for review"}]', - inputSchemaJson: '{"contract_text":"string","target_schema":"object","vendor_name":"string"}', - outputSchemaJson: '{"fields":"object","confidence":"object","exceptions":"array"}', - exampleInput: "MSA text with renewal terms, termination notice, and SLA obligations.", - exampleOutput: "Structured contract fields with confidence and review flags.", - riskLevel: "HIGH", - }, - }, - EVALUATION_CHECKLIST: { - type: "EVALUATION_CHECKLIST", - departmentName: "Governance", - captureLabel: "Review criteria", - contentLabel: "Evaluation Rubric", - description: "A quality gate for reviewing AI outputs, tools, vendors, or prompts.", - requiredInputs: ["Artifact to review", "Criteria", "Business context"], - expectedOutputs: ["Score", "Issues", "Decision recommendation"], - rawCapture: - "Governance reviews new AI tools before teams use them. The checklist should cover data access, privacy, hallucination risk, auditability, and owner readiness.", - template: { - title: "Vendor AI Tool Review Checklist", - summary: "Scores proposed AI tools across privacy, data access, auditability, hallucination risk, and operational ownership.", - assetType: "EVALUATION_CHECKLIST", - useCase: "AI vendor review", - businessProcess: "AI governance", - aiTool: "Claude", - tagNames: "evaluation, vendor-review, governance", - promptTemplate: - "Evaluate {{tool_profile}} against {{review_criteria}}. Score each area, flag blockers, and recommend approve, pilot, or reject.", - workflowStepsJson: - '[{"name":"Collect tool profile"},{"name":"Check data access"},{"name":"Score risk criteria"},{"name":"Flag blockers"},{"name":"Record decision"}]', - inputSchemaJson: '{"tool_profile":"string","review_criteria":"array","business_context":"string"}', - outputSchemaJson: '{"score":"number","findings":"array","blockers":"array","recommendation":"string"}', - exampleInput: "AI meeting note vendor requesting calendar and transcript access.", - exampleOutput: "Risk score, blockers, and pilot recommendation.", - riskLevel: "HIGH", - }, - }, - PLAYBOOK: { - type: "PLAYBOOK", - departmentName: "Sales", - captureLabel: "Operating playbook notes", - contentLabel: "Playbook Instructions", - description: "A reusable operating guide for a business situation with talk tracks and actions.", - requiredInputs: ["Scenario", "Context", "Allowed actions"], - expectedOutputs: ["Recommended path", "Talk track", "Follow-up actions"], - rawCapture: - "Sales needs an objection playbook for common competitor claims. It should use product facts, approved differentiators, discovery questions, and follow-up assets.", - template: { - title: "Competitive Objection Handling Playbook", - summary: "Turns competitor context into approved differentiators, discovery questions, objection handling, and follow-up assets.", - assetType: "PLAYBOOK", - useCase: "Competitive enablement", - businessProcess: "Sales enablement", - aiTool: "Claude", - tagNames: "playbook, competitive, sales", - promptTemplate: - "Build an objection handling play from {{competitor_claim}}, {{customer_context}}, and {{approved_differentiators}}.", - workflowStepsJson: - '[{"name":"Capture competitor claim"},{"name":"Match approved facts"},{"name":"Draft discovery questions"},{"name":"Suggest talk track"},{"name":"Attach follow-up assets"}]', - inputSchemaJson: '{"competitor_claim":"string","customer_context":"string","approved_differentiators":"array"}', - outputSchemaJson: '{"talk_track":"string","questions":"array","follow_up_assets":"array","risks":"array"}', - exampleInput: "Prospect says competitor has faster onboarding and cheaper analytics.", - exampleOutput: "Objection play with approved differentiators and discovery questions.", - riskLevel: "MEDIUM", - }, - }, - HANDOVER_PACK: { - type: "HANDOVER_PACK", - departmentName: "People Operations", - captureLabel: "Role transition context", - contentLabel: "Handover Pack Instructions", - description: "A transfer package that preserves AI capability ownership across onboarding or offboarding.", - requiredInputs: ["Owned assets", "Recurring workflows", "Successor context"], - expectedOutputs: ["Handover plan", "At-risk assets", "Action owners"], - rawCapture: - "When someone leaves or joins a role, managers need a pack of owned AI assets, recurring workflows, missing backup owners, and first-week actions.", - template: { - title: "Role Transition Handover Pack", - summary: "Builds a role transition pack with owned assets, recurring AI workflows, backup owner gaps, and action checklist.", - assetType: "HANDOVER_PACK", - useCase: "Role transition continuity", - businessProcess: "People operations", - aiTool: "Claude", - tagNames: "handover, onboarding, offboarding", - promptTemplate: - "Build a transition pack from {{owned_assets}}, {{recurring_workflows}}, {{access_notes}}, and {{successor_context}}.", - workflowStepsJson: - '[{"name":"List owned assets"},{"name":"Detect backup gaps"},{"name":"Summarize recurring workflows"},{"name":"Assign continuity actions"},{"name":"Generate transition pack"}]', - inputSchemaJson: '{"owned_assets":"array","recurring_workflows":"string","access_notes":"string","successor_context":"string"}', - outputSchemaJson: '{"handover_pack":"markdown","at_risk_assets":"array","actions":"array"}', - exampleInput: "Departing customer success manager with eight owned workflows.", - exampleOutput: "Handover pack, at-risk assets, and owner actions.", - riskLevel: "MEDIUM", - }, - }, - GOVERNANCE_GUARDRAIL: { - type: "GOVERNANCE_GUARDRAIL", - departmentName: "Governance", - captureLabel: "Policy or safety rule", - contentLabel: "Guardrail Instructions", - description: "A reusable policy check for privacy, compliance, brand, or model-risk boundaries.", - requiredInputs: ["Prompt or output", "Sharing context", "Policy rule"], - expectedOutputs: ["Risk classification", "Redactions", "Escalation"], - rawCapture: - "Before teams share AI-generated customer summaries externally, we need to detect PII, sensitive attributes, unsupported claims, and required redactions.", - template: { - title: "External Sharing Safety Guardrail", - summary: "Checks prompts and outputs before external sharing for PII, unsupported claims, redaction gaps, and escalation needs.", - assetType: "GOVERNANCE_GUARDRAIL", - useCase: "External output safety review", - businessProcess: "AI governance", - aiTool: "OpenAI GPT-4o", - tagNames: "guardrail, pii, external-sharing", - promptTemplate: - "Review {{prompt_or_output}} for {{sharing_context}}. Detect PII, sensitive attributes, unsupported claims, and redaction gaps.", - workflowStepsJson: - '[{"name":"Inspect prompt or output"},{"name":"Detect sensitive data"},{"name":"Classify risk"},{"name":"Recommend redaction"},{"name":"Escalate high-risk cases"}]', - inputSchemaJson: '{"prompt_or_output":"string","sharing_context":"string","policy_rule":"string"}', - outputSchemaJson: '{"risk":"string","findings":"array","redacted_version":"string","escalation":"string"}', - exampleInput: "Customer-facing summary containing names, emails, and account identifiers.", - exampleOutput: "Risk findings, redacted version, and escalation path.", - riskLevel: "HIGH", - }, - }, - COPILOT: { - type: "COPILOT", - departmentName: "Customer Success", - captureLabel: "Copilot usage scenario", - contentLabel: "Copilot Instructions", - description: "An assistant that stays in the user's workflow and suggests next actions or drafts.", - requiredInputs: ["Live context", "User goal", "Knowledge sources"], - expectedOutputs: ["Suggested response", "Next action", "Source hints"], - rawCapture: - "Support agents need an in-workflow copilot that reads a ticket, checks similar resolutions, drafts a reply, and suggests escalation when confidence is low.", - template: { - title: "Support Reply Copilot", - summary: "Suggests support replies from ticket context, customer tier, product area, similar cases, and tone guidelines.", - assetType: "COPILOT", - useCase: "Support response assistance", - businessProcess: "Customer service operations", - aiTool: "ChatGPT", - tagNames: "copilot, support, customer-service", - promptTemplate: - "Suggest a support reply using {{ticket_context}}, {{customer_tier}}, {{product_area}}, and {{resolution_history}}. Escalate if confidence is low.", - workflowStepsJson: - '[{"name":"Read ticket context"},{"name":"Retrieve similar cases"},{"name":"Draft reply"},{"name":"Check tone and policy"},{"name":"Agent approves response"}]', - inputSchemaJson: '{"ticket_context":"string","customer_tier":"string","product_area":"string","resolution_history":"array"}', - outputSchemaJson: '{"reply":"string","confidence":"string","related_articles":"array","escalation":"string"}', - exampleInput: "Priority customer asks about delayed analytics export.", - exampleOutput: "Support reply with confidence, related articles, and escalation note.", - riskLevel: "MEDIUM", - }, - }, -} - -export function getAssetTypeSpec(assetType: AssetType) { - return assetTypeSpecs[assetType] -} - -export function assetTypeSummary(assetType: AssetType) { - const spec = getAssetTypeSpec(assetType) - return `${formatAssetType(assetType)}: ${spec.description}` -} diff --git a/web/src/features/assets/asset-type.ts b/web/src/features/assets/asset-type.ts deleted file mode 100644 index c2e8947c..00000000 --- a/web/src/features/assets/asset-type.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { AssetType } from "@/lib/api" - -export const assetTypes: AssetType[] = [ - "PROMPT_TEMPLATE", - "WORKFLOW_AUTOMATION", - "AI_AGENT", - "KNOWLEDGE_BOT", - "ANALYTICS_BRIEF", - "CONTENT_GENERATOR", - "DATA_EXTRACTION", - "EVALUATION_CHECKLIST", - "PLAYBOOK", - "HANDOVER_PACK", - "GOVERNANCE_GUARDRAIL", - "COPILOT", -] - -export const assetTypeLabels: Record = { - PROMPT_TEMPLATE: "Prompt Template", - WORKFLOW_AUTOMATION: "Workflow Automation", - AI_AGENT: "AI Agent", - KNOWLEDGE_BOT: "Knowledge Bot", - ANALYTICS_BRIEF: "Analytics Brief", - CONTENT_GENERATOR: "Content Generator", - DATA_EXTRACTION: "Data Extraction", - EVALUATION_CHECKLIST: "Evaluation Checklist", - PLAYBOOK: "Playbook", - HANDOVER_PACK: "Handover Pack", - GOVERNANCE_GUARDRAIL: "Governance Guardrail", - COPILOT: "Copilot", -} - -export function formatAssetType(assetType?: AssetType | null) { - return assetType ? assetTypeLabels[assetType] : "Workflow Automation" -} diff --git a/web/src/features/assets/demo-data.ts b/web/src/features/assets/demo-data.ts deleted file mode 100644 index 34c49f1d..00000000 --- a/web/src/features/assets/demo-data.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AssetType, CapabilityAsset, RiskLevel } from "@/lib/api" - -export type DraftForm = { - title: string - summary: string - assetType: AssetType - useCase: string - businessProcess: string - aiTool: string - tagNames: string - promptTemplate: string - workflowStepsJson: string - inputSchemaJson: string - outputSchemaJson: string - exampleInput: string - exampleOutput: string - riskLevel: RiskLevel -} - -export const demo = { - organizationId: "11111111-1111-1111-1111-111111111111", - salesDepartmentId: "22222222-2222-2222-2222-222222222222", - ownerUserId: "44444444-4444-4444-4444-444444444444", - backupOwnerUserId: "55555555-5555-5555-5555-555555555555", -} - -export const initialRawCapture = `After each B2B product demo, paste call notes into Claude. -It writes a concise follow-up email, extracts promised next steps, flags customer concerns, and gives the sales rep a clean handoff summary for CRM. -The workflow touches customer context but no payment data.` - -export const initialForm: DraftForm = { - title: "Post-demo follow-up email", - summary: "Generates a concise follow-up email after a B2B product demo.", - assetType: "CONTENT_GENERATOR", - useCase: "Sales follow-up", - businessProcess: "Sales", - aiTool: "Claude", - tagNames: "sales, follow-up, email", - promptTemplate: - "Use these demo notes: {{notes}}. Write a short follow-up email, list promised next steps, and flag customer concerns.", - workflowStepsJson: - '[{"name":"Paste demo notes"},{"name":"Generate email and action list"},{"name":"Sales rep reviews before sending"}]', - inputSchemaJson: '{"notes":"string","account":"string"}', - outputSchemaJson: '{"email":"string","nextSteps":"string[]","risks":"string[]"}', - exampleInput: "The buyer asked about onboarding time and requested a security checklist.", - exampleOutput: "Draft email, next steps, and concerns ready for rep review.", - riskLevel: "MEDIUM", -} - -export function buildMetrics(assets: CapabilityAsset[]) { - return { - total: assets.length, - approved: assets.filter((asset) => asset.status === "APPROVED").length, - inReview: assets.filter((asset) => asset.status === "IN_REVIEW").length, - missingBackup: assets.filter((asset) => !asset.backupOwnerUserId).length, - usage: assets.reduce((sum, asset) => sum + asset.usageCount, 0), - } -} diff --git a/web/src/features/assets/status-badge.tsx b/web/src/features/assets/status-badge.tsx deleted file mode 100644 index 44f09e3d..00000000 --- a/web/src/features/assets/status-badge.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Badge } from "@/components/ui/badge" -import type { AssetStatus } from "@/lib/api" - -export function StatusBadge({ status }: { status: AssetStatus }) { - if (status === "APPROVED") return Approved - if (status === "IN_REVIEW") return Needs Review - if (status === "DEPRECATED") return Deprecated - if (status === "REJECTED") return Rejected - return Draft -} diff --git a/web/src/features/assets/use-assets.ts b/web/src/features/assets/use-assets.ts deleted file mode 100644 index e6f228ef..00000000 --- a/web/src/features/assets/use-assets.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { - createAsset, - assignBackupOwner, - getAsset, - getKnowledgeGraph, - listAssets, - listVersions, - normalizeAsset, - recordUsage, - updateAssetStatus, - type AssetType, - type AssetStatus, - type CreateAssetPayload, -} from "@/lib/api" - -export function useAssets(status?: AssetStatus | "", query?: string, assetType?: AssetType | "") { - return useQuery({ - queryKey: ["assets", status || "", query || "", assetType || ""], - queryFn: () => listAssets(status || undefined, query || undefined, assetType || undefined), - }) -} - -export function useAssetVersions(assetId?: string | null) { - return useQuery({ - queryKey: ["asset-versions", assetId], - queryFn: () => listVersions(assetId ?? ""), - enabled: Boolean(assetId), - }) -} - -export function useAsset(assetId?: string | null) { - return useQuery({ - queryKey: ["asset", assetId], - queryFn: () => getAsset(assetId ?? ""), - enabled: Boolean(assetId), - }) -} - -export function useKnowledgeGraph(options?: { query?: string; focusAssetId?: string; depth?: number }) { - return useQuery({ - queryKey: ["knowledge-graph", options?.query ?? "", options?.focusAssetId ?? "", options?.depth ?? 0], - queryFn: () => getKnowledgeGraph(options), - }) -} - -export function useCreateAsset() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (payload: CreateAssetPayload) => createAsset(payload), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["assets"] }), - }) -} - -export function useNormalizeAsset() { - return useMutation({ - mutationFn: ({ rawText, aiTool, businessProcess }: { rawText: string; aiTool?: string; businessProcess?: string }) => - normalizeAsset(rawText, aiTool, businessProcess), - }) -} - -export function useRecordUsage() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (assetId: string) => recordUsage(assetId, "USED"), - onSuccess: (_data, assetId) => { - queryClient.invalidateQueries({ queryKey: ["assets"] }) - queryClient.invalidateQueries({ queryKey: ["asset", assetId] }) - }, - }) -} - -export function useAssetStatusAction() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ assetId, action }: { assetId: string; action: "submit-review" | "approve" | "reject" | "deprecate" }) => - updateAssetStatus(assetId, action), - onSuccess: (asset) => { - queryClient.invalidateQueries({ queryKey: ["assets"] }) - queryClient.invalidateQueries({ queryKey: ["asset", asset.id] }) - }, - }) -} - -export function useAssignBackupOwner() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ assetId, backupOwnerUserId }: { assetId: string; backupOwnerUserId: string }) => - assignBackupOwner(assetId, backupOwnerUserId), - onSuccess: (asset) => { - queryClient.invalidateQueries({ queryKey: ["assets"] }) - queryClient.invalidateQueries({ queryKey: ["asset", asset.id] }) - }, - }) -} diff --git a/web/src/features/assets/workflow-diagram.tsx b/web/src/features/assets/workflow-diagram.tsx deleted file mode 100644 index 3beb00cc..00000000 --- a/web/src/features/assets/workflow-diagram.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Background, Controls, ReactFlow, type Edge, type Node } from "@xyflow/react" - -type WorkflowDiagramProps = { - steps?: string | null -} - -export function WorkflowDiagram({ steps }: WorkflowDiagramProps) { - const labels = parseStepLabels(steps) - const nodes: Node[] = labels.map((label, index) => ({ - id: String(index + 1), - position: { x: index * 230, y: index % 2 === 0 ? 20 : 120 }, - data: { label }, - type: "default", - })) - const edges: Edge[] = labels.slice(1).map((_, index) => ({ - id: `e${index + 1}-${index + 2}`, - source: String(index + 1), - target: String(index + 2), - animated: true, - })) - - return ( -
- - - - -
- ) -} - -function parseStepLabels(steps?: string | null) { - if (!steps) { - return ["Capture input", "Run model", "Review output", "Publish capability"] - } - - try { - const parsed = JSON.parse(steps) as Array<{ name?: string; label?: string; title?: string }> - const labels = parsed - .map((step) => step.name ?? step.label ?? step.title) - .filter((label): label is string => Boolean(label)) - return labels.length ? labels : ["Capture input", "Run model", "Review output", "Publish capability"] - } catch { - return steps - .split(/\r?\n|->|,/) - .map((step) => step.trim()) - .filter(Boolean) - .slice(0, 6) - } -} diff --git a/web/src/features/organization/use-organization-context.ts b/web/src/features/organization/use-organization-context.ts deleted file mode 100644 index 3e9e966b..00000000 --- a/web/src/features/organization/use-organization-context.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useMemo } from "react" -import { useQuery } from "@tanstack/react-query" -import { getOrganizationContext, type Department, type OrgUser } from "@/lib/api" - -export function useOrganizationContext() { - return useQuery({ - queryKey: ["organization-context"], - queryFn: getOrganizationContext, - }) -} - -export function useOrganizationLookups() { - const context = useOrganizationContext() - const lookups = useMemo(() => { - const departments = context.data?.departments ?? [] - const users = context.data?.users ?? [] - return { - departments, - users, - departmentById: new Map(departments.map((department) => [department.id, department])), - userById: new Map(users.map((user) => [user.id, user])), - } - }, [context.data]) - - return { ...context, ...lookups } -} - -export function departmentName(departments: Map, id?: string | null) { - return id ? departments.get(id)?.name ?? "Unassigned" : "Unassigned" -} - -export function userName(users: Map, id?: string | null) { - return id ? users.get(id)?.name ?? "Unassigned" : "Unassigned" -} - -export function userInitials(user?: OrgUser) { - if (!user) return "OM" - return user.name - .split(" ") - .map((part) => part[0]) - .join("") - .slice(0, 2) - .toUpperCase() -} diff --git a/web/src/index.css b/web/src/index.css index 65b0bcf9..ff88d94b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -120,7 +120,7 @@ html, body, #root { - min-height: 100%; + min-height: 100dvh; } body { @@ -129,5 +129,6 @@ color: var(--color-foreground); font-family: system-ui, sans-serif; -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts deleted file mode 100644 index 8e81562d..00000000 --- a/web/src/lib/api.ts +++ /dev/null @@ -1,284 +0,0 @@ -export type AssetStatus = 'DRAFT' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'DEPRECATED' -export type AssetVisibility = 'PRIVATE' | 'TEAM' | 'ORGANIZATION' -export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' -export type UsageEventType = 'VIEWED' | 'COPIED' | 'USED' | 'SHARED' -export type OrgUserRole = 'EMPLOYEE' | 'TEAM_LEAD' | 'ADMIN' -export type AssetType = - | 'PROMPT_TEMPLATE' - | 'WORKFLOW_AUTOMATION' - | 'AI_AGENT' - | 'KNOWLEDGE_BOT' - | 'ANALYTICS_BRIEF' - | 'CONTENT_GENERATOR' - | 'DATA_EXTRACTION' - | 'EVALUATION_CHECKLIST' - | 'PLAYBOOK' - | 'HANDOVER_PACK' - | 'GOVERNANCE_GUARDRAIL' - | 'COPILOT' - -export type GraphNodeKind = 'ASSET' | 'ASSET_TYPE' | 'DEPARTMENT' | 'USER' | 'TAG' -export type GraphEdgeKind = - | 'HAS_TYPE' - | 'BELONGS_TO' - | 'OWNED_BY' - | 'BACKED_UP_BY' - | 'TAGGED_WITH' - | 'RELATED_BY_TAG' - | 'RELATED_BY_OWNER' - | 'RELATED_BY_PROCESS' - -export type CapabilityAsset = { - id: string - organizationId: string - departmentId: string | null - title: string - summary: string - assetType: AssetType - useCase: string | null - businessProcess: string | null - aiTool: string | null - tagNames: string | null - ownerUserId: string | null - backupOwnerUserId: string | null - status: AssetStatus - visibility: AssetVisibility - riskLevel: RiskLevel | null - currentVersionId: string | null - createdByUserId: string | null - usageCount: number - createdAt: string - updatedAt: string -} - -export type Department = { - id: string - organizationId: string - name: string -} - -export type OrgUser = { - id: string - organizationId: string - departmentId: string | null - name: string - email: string - role: OrgUserRole -} - -export type OrganizationContext = { - organizationId: string - departments: Department[] - users: OrgUser[] -} - -export type AssetVersion = { - id: string - assetId: string - versionNumber: number - promptTemplate: string | null - workflowStepsJson: string | null - inputSchemaJson: string | null - outputSchemaJson: string | null - exampleInput: string | null - exampleOutput: string | null - changeNote: string | null - createdByUserId: string | null - createdAt: string -} - -export type KnowledgeGraphNode = { - id: string - label: string - kind: GraphNodeKind - detail: string | null - assetId: string | null - assetType: AssetType | null - status: AssetStatus | null - weight: number -} - -export type KnowledgeGraphEdge = { - id: string - source: string - target: string - kind: GraphEdgeKind - label: string - weight: number -} - -export type KnowledgeGraph = { - nodes: KnowledgeGraphNode[] - edges: KnowledgeGraphEdge[] - focusNodeId: string | null - depth: number -} - -export type CreateAssetPayload = { - departmentId?: string - title: string - summary: string - assetType: AssetType - useCase?: string - businessProcess?: string - aiTool?: string - tagNames?: string - ownerUserId?: string - backupOwnerUserId?: string - visibility: AssetVisibility - riskLevel: RiskLevel - promptTemplate?: string - workflowStepsJson?: string - inputSchemaJson?: string - outputSchemaJson?: string - exampleInput?: string - exampleOutput?: string -} - -export type AiDraftResponse = { - aiEnabled: boolean - source: string - note: string - title: string - summary: string - assetType: AssetType - useCase: string - businessProcess: string - aiTool: string - tagNames: string - riskLevel: RiskLevel - promptTemplate: string - workflowStepsJson: string - inputSchemaJson: string - outputSchemaJson: string - exampleInput: string - exampleOutput: string -} - -import { getBrowserCsrfToken } from './hey-api' - -const jsonHeaders = { - 'Content-Type': 'application/json', -} - -export const DEFAULT_ORGANIZATION_ID = '11111111-1111-1111-1111-111111111111' - -export type Me = { - authenticated: boolean - subject: string | null - email: string | null - name: string | null - authorizationProvider: 'openfga' - userId: string - organizationId: string - departmentId: string | null -} - -async function request(path: string, init?: RequestInit): Promise { - const headers = new Headers(init?.headers) - const method = init?.method?.toUpperCase() ?? 'GET' - if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) { - const { data } = await getBrowserCsrfToken({ throwOnError: true }) - if (!data?.headerName || !data.token) { - throw new Error('The server did not issue a CSRF token.') - } - headers.set(data.headerName, data.token) - } - - const response = await fetch(path, { - ...init, - credentials: 'same-origin', - headers, - }) - if (!response.ok) { - const body = await response.text() - throw new Error(body || `Request failed with ${response.status}`) - } - return response.json() as Promise -} - -export function getMe() { - return request('/api/me') -} - -export function listAssets(status?: AssetStatus, query?: string, assetType?: AssetType) { - const params = new URLSearchParams() - if (status) { - params.set('status', status) - } - if (assetType) { - params.set('assetType', assetType) - } - if (query) { - params.set('q', query) - } - const suffix = params.size > 0 ? `?${params.toString()}` : '' - return request(`/api/assets${suffix}`) -} - -export function getAsset(assetId: string) { - return request(`/api/assets/${assetId}`) -} - -export function getOrganizationContext() { - return request('/api/organization/context') -} - -export function createAsset(payload: CreateAssetPayload) { - return request('/api/assets', { - method: 'POST', - headers: jsonHeaders, - body: JSON.stringify(payload), - }) -} - -export function normalizeAsset(rawText: string, aiTool?: string, businessProcess?: string) { - return request('/api/ai/assets/normalize', { - method: 'POST', - headers: jsonHeaders, - body: JSON.stringify({ rawText, aiTool, businessProcess }), - }) -} - -export function updateAssetStatus(assetId: string, action: 'submit-review' | 'approve' | 'reject' | 'deprecate') { - return request(`/api/assets/${assetId}/${action}`, { - method: 'PATCH', - headers: jsonHeaders, - body: JSON.stringify({ comment: `Updated through OrgMemory web: ${action}` }), - }) -} - -export function assignBackupOwner(assetId: string, backupOwnerUserId: string) { - return request(`/api/assets/${assetId}/backup-owner`, { - method: 'PATCH', - headers: jsonHeaders, - body: JSON.stringify({ backupOwnerUserId }), - }) -} - -export function recordUsage(assetId: string, eventType: UsageEventType) { - return request<{ assetId: string; usageCount: number }>(`/api/assets/${assetId}/usage`, { - method: 'POST', - headers: jsonHeaders, - body: JSON.stringify({ eventType }), - }) -} - -export function listVersions(assetId: string) { - return request(`/api/assets/${assetId}/versions`) -} - -export function getKnowledgeGraph(options?: { query?: string; focusAssetId?: string; depth?: number }) { - const params = new URLSearchParams() - if (options?.query) { - params.set('q', options.query) - } - if (options?.focusAssetId) { - params.set('focusAssetId', options.focusAssetId) - } - if (options?.depth) { - params.set('depth', String(options.depth)) - } - const suffix = params.size > 0 ? `?${params.toString()}` : '' - return request(`/api/graph${suffix}`) -} diff --git a/web/src/lib/query-client.ts b/web/src/lib/query-client.ts new file mode 100644 index 00000000..cbd178c0 --- /dev/null +++ b/web/src/lib/query-client.ts @@ -0,0 +1,17 @@ +import { QueryCache, QueryClient } from "@tanstack/react-query" +import { toast } from "sonner" + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + }, + }, + queryCache: new QueryCache({ + onError: (_error, query) => { + if (query.meta?.silent || query.state.data === undefined) return + toast.error("Could not refresh OrgMemory data.", { id: `query-${query.queryHash}` }) + }, + }), +}) diff --git a/web/src/main.tsx b/web/src/main.tsx index 25651e17..33ab81bb 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,37 +1,55 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import { RouterProvider } from '@tanstack/react-router' -import { ThemeProvider } from 'next-themes' -import { Toaster, toast } from 'sonner' +import { QueryClientProvider, QueryErrorResetBoundary } from "@tanstack/react-query" +import { RouterProvider } from "@tanstack/react-router" +import { lazy, StrictMode, Suspense } from "react" +import { createRoot } from "react-dom/client" +import { ErrorBoundary, type FallbackProps } from "react-error-boundary" +import { ThemeProvider } from "next-themes" +import { Toaster } from "sonner" -import './lib/api-client' -import { router } from './router' -import './index.css' +import "./lib/api-client" +import "./index.css" +import { ApplicationError } from "@/components/states/application-error" +import { queryClient } from "@/lib/query-client" +import { router } from "@/router" -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 30_000, - retry: 1, - }, - }, - queryCache: new QueryCache({ - onError: (_error, query) => { - if (query.meta?.silent) return - toast.error('Could not load OrgMemory data.', { id: `query-${query.queryHash}` }) - }, - }), +const QueryDevtools = lazy(async () => { + if (!import.meta.env.DEV) return { default: () => null } + const module = await import("@tanstack/react-query-devtools") + return { default: module.ReactQueryDevtools } }) -createRoot(document.getElementById('root')!).render( +function ApplicationCrash({ error, resetErrorBoundary }: FallbackProps) { + return ( + + ) +} + +const rootElement = document.getElementById("root") + +if (!rootElement) { + throw new Error("OrgMemory root element was not found.") +} + +createRoot(rootElement).render( - + + {({ reset }) => ( + + + + )} + - + + + , diff --git a/web/src/pages/analytics.tsx b/web/src/pages/analytics.tsx deleted file mode 100644 index cae23020..00000000 --- a/web/src/pages/analytics.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { DashboardPage } from "@/pages/dashboard" - -export function AnalyticsPage() { - return -} diff --git a/web/src/pages/ask-memory.tsx b/web/src/pages/ask-memory.tsx deleted file mode 100644 index 34ae3221..00000000 --- a/web/src/pages/ask-memory.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useChat } from "@ai-sdk/react" -import { Link } from "@tanstack/react-router" -import { DefaultChatTransport } from "ai" -import { Bot, Check, Sparkles } from "lucide-react" -import { useMemo, useState } from "react" -import { toast } from "sonner" -import { PageTitle } from "@/components/layout/page-title" -import { - Conversation, - ConversationContent, - ConversationEmptyState, - ConversationScrollButton, -} from "@/components/ai-elements/conversation" -import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message" -import { - PromptInput, - PromptInputBody, - PromptInputFooter, - PromptInputSubmit, - PromptInputTextarea, - type PromptInputMessage, -} from "@/components/ai-elements/prompt-input" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { formatAssetType } from "@/features/assets/asset-type" -import { StatusBadge } from "@/features/assets/status-badge" -import { useAssets, useRecordUsage } from "@/features/assets/use-assets" -import { getBrowserCsrfToken } from "@/lib/hey-api" -import { useOrganizationLookups, userName } from "@/features/organization/use-organization-context" - -export function AskMemoryPage() { - const [conversationId] = useState(() => crypto.randomUUID()) - const [lastQuery, setLastQuery] = useState("") - const { data, isError, isLoading } = useAssets() - const { userById } = useOrganizationLookups() - const usage = useRecordUsage() - const matches = useMemo(() => rankAssets(data ?? [], lastQuery).slice(0, 3), [data, lastQuery]) - const { messages, sendMessage, status, stop } = useChat({ - transport: new DefaultChatTransport({ - api: "/api/ai/chat", - prepareSendMessagesRequest: async ({ messages }) => { - const last = messages.at(-1) - const text = (last?.parts ?? []) - .filter((part) => part.type === "text") - .map((part) => (part as { text: string }).text) - .join("") - const { data: csrf } = await getBrowserCsrfToken({ throwOnError: true }) - const headers = csrf?.headerName && csrf.token ? { [csrf.headerName]: csrf.token } : undefined - return { body: { message: text, conversationId }, headers } - }, - }), - }) - - function onSubmit(message: PromptInputMessage) { - if (message.text.trim()) { - setLastQuery(message.text) - sendMessage({ text: message.text }) - } - } - - return ( -
- - -
- - - - {messages.length === 0 ? ( - } - title="Ask about organizational AI memory" - description="Try: what approved workflows do we have for customer feedback analysis?" - /> - ) : ( - messages.map((message) => ( - - - {message.parts.map((part, index) => { - if (part.type !== "text") return null - return {part.text} - })} - - - )) - )} - - - - - - - - - - Spring AI streaming via UI Message Stream - - - - - - - -
-
- ) -} - -function rankAssets( - assets: T[], - query: string, -) { - const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 2) - if (!tokens.length) { - return assets - } - - return [...assets].sort((left, right) => scoreAsset(right, tokens, query) - scoreAsset(left, tokens, query)) -} - -function scoreAsset(asset: { title: string; summary: string; useCase: string | null; businessProcess: string | null; tagNames: string | null; status: string; usageCount: number }, tokens: string[], query: string) { - const haystack = [ - asset.title, - asset.summary, - asset.useCase ?? "", - asset.businessProcess ?? "", - asset.tagNames ?? "", - asset.status, - ].join(" ").toLowerCase() - const normalizedQuery = query.toLowerCase() - let score = tokens.reduce((sum, token) => sum + (haystack.includes(token) ? 2 : 0), 0) - if (haystack.includes("customer feedback") && normalizedQuery.includes("customer feedback")) score += 8 - if (asset.status === "APPROVED") score += normalizedQuery.includes("approved") ? 6 : 2 - score += Math.min(asset.usageCount, 3) - return score -} diff --git a/web/src/pages/asset-detail.tsx b/web/src/pages/asset-detail.tsx deleted file mode 100644 index d97b85f1..00000000 --- a/web/src/pages/asset-detail.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { Link, useParams } from "@tanstack/react-router" -import { - ArrowLeft, - Check, - Copy, - FileText, - ShieldAlert, - ShieldCheck, - UserRoundCheck, -} from "lucide-react" -import { toast } from "sonner" -import { PageTitle } from "@/components/layout/page-title" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Separator } from "@/components/ui/separator" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { formatAssetType } from "@/features/assets/asset-type" -import { StatusBadge } from "@/features/assets/status-badge" -import { - useAsset, - useAssetStatusAction, - useAssetVersions, - useAssignBackupOwner, - useRecordUsage, -} from "@/features/assets/use-assets" -import { WorkflowDiagram } from "@/features/assets/workflow-diagram" -import { - departmentName, - useOrganizationLookups, - userName, -} from "@/features/organization/use-organization-context" -import type { AssetVersion, CapabilityAsset } from "@/lib/api" - -export function AssetDetailPage() { - const { assetId } = useParams({ strict: false }) as { assetId: string } - const asset = useAsset(assetId) - const versions = useAssetVersions(assetId) - const usage = useRecordUsage() - const statusAction = useAssetStatusAction() - const assignBackupOwner = useAssignBackupOwner() - const { departmentById, users, userById } = useOrganizationLookups() - - if (asset.isLoading) { - return ( -
- -
- - -
-
- ) - } - - if (asset.isError || !asset.data) { - return ( -
- - - - Asset unavailable - Check that the asset still exists, then return to the registry. - - -
- ) - } - - const currentAsset = asset.data - const currentVersion = versions.data?.[0] - const backupCandidate = users.find((user) => user.id !== currentAsset.ownerUserId)?.id - const canReview = currentAsset.status !== "APPROVED" && currentAsset.status !== "DEPRECATED" - - function runStatusAction(action: "submit-review" | "approve" | "deprecate") { - statusAction.mutate( - { assetId, action }, - { onSuccess: () => toast.success(`Asset ${action.replace("-", " ")} completed.`) }, - ) - } - - return ( -
-
- - -
- - - -
-
-
- - {formatAssetType(currentAsset.assetType)} - {currentAsset.riskLevel ? {currentAsset.riskLevel} risk : null} - {currentAsset.visibility.toLowerCase()} -
- {currentAsset.summary} -
- - - {currentAsset.status === "DRAFT" ? ( - - ) : null} - {canReview ? ( - - ) : null} - -
-
- - - - - - -
- -
-
- - - Workflow - Current version visualized from persisted workflow steps. - - - - - - - - - - - Prompt - Inputs - Governance - Versions - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- ) -} - -function MetaCard({ label, value, destructive }: { label: string; value: string; destructive?: boolean }) { - return ( -
-

{label}

-

{value}

-
- ) -} - -function DetailPanel({ title, value }: { title: string; value?: string | null }) { - return ( -
-

{title}

- -
- ) -} - -function CodeBlock({ value, compact }: { value: string; compact?: boolean }) { - return ( - -
{value}
-
- ) -} - -function GovernanceTable({ asset, department }: { asset: CapabilityAsset; department: string }) { - const rows = [ - ["Department", department], - ["Business process", asset.businessProcess ?? "Not captured"], - ["AI tool", asset.aiTool ?? "Tool agnostic"], - ["Risk level", asset.riskLevel ?? "Not assessed"], - ["Visibility", asset.visibility], - ["Tags", asset.tagNames ?? "No tags"], - ] - - return ( - - - {rows.map(([label, value]) => ( - - {label} - {value} - - ))} - -
- ) -} - -function VersionTable({ versions, userById }: { versions: AssetVersion[]; userById: Map }) { - if (!versions.length) { - return

No persisted versions loaded.

- } - - return ( - - - - Version - Change Note - Created By - Created - - - - {versions.map((version) => ( - - v{version.versionNumber} - {version.changeNote ?? "No note"} - {version.createdByUserId ? userById.get(version.createdByUserId)?.name ?? "Unassigned" : "Unassigned"} - {new Date(version.createdAt).toLocaleDateString()} - - ))} - -
- ) -} - -function OwnerLine({ label, value, destructive }: { label: string; value: string; destructive?: boolean }) { - return ( -
- {label} - {value} -
- ) -} diff --git a/web/src/pages/create-asset.tsx b/web/src/pages/create-asset.tsx deleted file mode 100644 index 8daf5586..00000000 --- a/web/src/pages/create-asset.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { useNavigate } from "@tanstack/react-router" -import { HelpCircle, Send, ShieldCheck, Sparkles } from "lucide-react" -import { useEffect, useState } from "react" -import { PageTitle } from "@/components/layout/page-title" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Textarea } from "@/components/ui/textarea" -import { initialForm, initialRawCapture, type DraftForm } from "@/features/assets/demo-data" -import { useAssetStatusAction, useCreateAsset, useNormalizeAsset } from "@/features/assets/use-assets" -import { assetTypes, formatAssetType } from "@/features/assets/asset-type" -import { assetTypeSummary, getAssetTypeSpec } from "@/features/assets/asset-type-specs" -import { useOrganizationContext } from "@/features/organization/use-organization-context" -import { type AssetType, type RiskLevel } from "@/lib/api" - -const aiToolOptions = [ - "ChatGPT", - "OpenAI GPT-4o", - "OpenAI Images", - "OpenAI Codex", - "Claude", - "Claude Code", - "Gemini", - "Perplexity", - "NotebookLM", - "GitHub Copilot", - "Cursor", - "v0", - "Lovable", - "Canva", - "Gamma", - "Runway", - "Descript", - "Midjourney", - "DALL-E", - "Ideogram", - "Stable Diffusion", - "Napkin AI", - "n8n", - "Zapier", - "Make", - "LangChain", - "LlamaIndex", - "Dify", - "Flowise", -] - -export function CreateAssetPage() { - const navigate = useNavigate() - const [rawCapture, setRawCapture] = useState(initialRawCapture) - const [form, setForm] = useState(initialForm) - const [departmentId, setDepartmentId] = useState("") - const [ownerUserId, setOwnerUserId] = useState("") - const [backupOwnerUserId, setBackupOwnerUserId] = useState("") - const [draftNote, setDraftNote] = useState("Generated preview will appear here after Spring AI enrichment.") - const organization = useOrganizationContext() - const normalize = useNormalizeAsset() - const create = useCreateAsset() - const statusAction = useAssetStatusAction() - const selectedSpec = getAssetTypeSpec(form.assetType) - - useEffect(() => { - const context = organization.data - if (!context) return - if (!departmentId) { - const matchingDepartment = context.departments.find((department) => department.name === selectedSpec.departmentName) - setDepartmentId((matchingDepartment ?? context.departments[0])?.id ?? "") - } - if (!ownerUserId) { - setOwnerUserId(context.users[0]?.id ?? "") - } - if (!backupOwnerUserId) { - setBackupOwnerUserId(context.users.find((user) => user.id !== context.users[0]?.id)?.id ?? "NONE") - } - }, [backupOwnerUserId, departmentId, organization.data, ownerUserId, selectedSpec.departmentName]) - - function applyAssetType(assetType: AssetType) { - const spec = getAssetTypeSpec(assetType) - setForm(spec.template) - setRawCapture(spec.rawCapture) - setDraftNote(assetTypeSummary(assetType)) - const matchingDepartment = organization.data?.departments.find((department) => department.name === spec.departmentName) - if (matchingDepartment) { - setDepartmentId(matchingDepartment.id) - } - } - - function onNormalize() { - normalize.mutate( - { rawText: rawCapture, aiTool: form.aiTool, businessProcess: form.businessProcess }, - { - onSuccess: (draft) => { - setForm({ - title: draft.title, - summary: draft.summary, - assetType: draft.assetType, - useCase: draft.useCase, - businessProcess: draft.businessProcess, - aiTool: draft.aiTool, - tagNames: draft.tagNames, - riskLevel: draft.riskLevel, - promptTemplate: draft.promptTemplate, - workflowStepsJson: draft.workflowStepsJson, - inputSchemaJson: draft.inputSchemaJson, - outputSchemaJson: draft.outputSchemaJson, - exampleInput: draft.exampleInput, - exampleOutput: draft.exampleOutput, - }) - setDraftNote(draft.note) - }, - }, - ) - } - - function buildPayload() { - const ownerId = ownerUserId || organization.data?.users[0]?.id - const backupOwnerId = - backupOwnerUserId === "NONE" - ? undefined - : backupOwnerUserId || organization.data?.users.find((user) => user.id !== ownerId)?.id - return { - departmentId: departmentId || organization.data?.departments[0]?.id, - title: form.title, - summary: form.summary, - assetType: form.assetType, - useCase: form.useCase, - businessProcess: form.businessProcess, - aiTool: form.aiTool, - tagNames: form.tagNames, - ownerUserId: ownerId, - backupOwnerUserId: backupOwnerId, - visibility: "TEAM" as const, - riskLevel: form.riskLevel, - promptTemplate: form.promptTemplate, - workflowStepsJson: form.workflowStepsJson, - inputSchemaJson: form.inputSchemaJson, - outputSchemaJson: form.outputSchemaJson, - exampleInput: form.exampleInput, - exampleOutput: form.exampleOutput, - } - } - - function onSaveDraft() { - create.mutate(buildPayload(), { onSuccess: () => navigate({ to: "/registry" }) }) - } - - function onSubmitForReview() { - create.mutate( - buildPayload(), - { - onSuccess: (asset) => { - statusAction.mutate( - { assetId: asset.id, action: "submit-review" }, - { onSuccess: () => navigate({ to: "/review" }) }, - ) - }, - }, - ) - } - - return ( -
- - -
- - - Asset Details - Describe the prompt, workflow, inputs, and expected output. - - - - - {formatAssetType(form.assetType)} capture - - {selectedSpec.description} - - - -
- - setForm({ ...form, title: event.target.value })} /> - - - - - - setForm({ ...form, useCase: event.target.value })} /> - - - - - - - - - setForm({ ...form, businessProcess: event.target.value })} /> - - - - - - - - - - - - setForm({ ...form, tagNames: event.target.value })} /> - -
- - -