From 39ef184b53adf4e679f06c6136d9f802558ddae9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 17 Mar 2026 10:23:56 -0400 Subject: [PATCH] feat: Add Mantine theme, 12 atom components, and 25 molecule components with Storybook stories - Mantine theme with component extensions (Badge, Card, Paper, Button, etc.) - CSS custom properties for design tokens (--inspector-* vars) - Dark/light mode support with auto detection - 12 atoms: StatusIndicator, AnnotationBadge, CapabilityItem, LogEntry, ProgressDisplay, ContentViewer, ListChangedIndicator, CopyButton, ConnectionToggle, TransportBadge, InlineError, MessageBubble - 25 molecules: ServerCard, AddServerMenu, SchemaForm, ToolListItem, ToolDetailPanel, ResultPanel, ResourceListItem, ResourcePreviewPanel, ResourceTemplateInput, PromptArgumentsForm, PromptMessagesDisplay, LogControls, TaskCard, HistoryEntry, ServerInfoContent, ServerSettingsForm, RootsTable, SamplingRequestPanel, ElicitationFormPanel, ElicitationUrlPanel, InlineSamplingRequest, InlineElicitationRequest, PendingClientRequests, ImportServerJsonPanel, ExperimentalFeaturesPanel - Exclude storybook-static from version control - Exclude .stories.tsx from app tsconfig (Storybook-only modules) Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 159 + CLAUDE.md | 2 + clients/web/.gitignore | 1 + clients/web/.storybook/preview.tsx | 7 +- clients/web/eslint.config.js | 2 +- clients/web/index.html | 2 +- clients/web/src/App.css | 105 + clients/web/src/App.tsx | 34 +- .../AnnotationBadge.stories.tsx | 52 + .../atoms/AnnotationBadge/AnnotationBadge.tsx | 27 + .../CapabilityItem/CapabilityItem.stories.tsx | 32 + .../atoms/CapabilityItem/CapabilityItem.tsx | 18 + .../ConnectionToggle.stories.tsx | 46 + .../ConnectionToggle/ConnectionToggle.tsx | 19 + .../ContentViewer/ContentViewer.stories.tsx | 78 + .../atoms/ContentViewer/ContentViewer.tsx | 43 + .../atoms/CopyButton/CopyButton.stories.tsx | 23 + .../atoms/CopyButton/CopyButton.tsx | 19 + .../atoms/InlineError/InlineError.stories.tsx | 50 + .../atoms/InlineError/InlineError.tsx | 43 + .../ListChangedIndicator.stories.tsx | 25 + .../ListChangedIndicator.tsx | 27 + .../atoms/LogEntry/LogEntry.stories.tsx | 83 + .../components/atoms/LogEntry/LogEntry.tsx | 72 + .../MessageBubble/MessageBubble.stories.tsx | 48 + .../atoms/MessageBubble/MessageBubble.tsx | 44 + .../ProgressDisplay.stories.tsx | 39 + .../atoms/ProgressDisplay/ProgressDisplay.tsx | 30 + .../StatusIndicator.stories.tsx | 42 + .../atoms/StatusIndicator/StatusIndicator.tsx | 49 + .../TransportBadge/TransportBadge.stories.tsx | 22 + .../atoms/TransportBadge/TransportBadge.tsx | 13 + .../AddServerMenu/AddServerMenu.stories.tsx | 18 + .../molecules/AddServerMenu/AddServerMenu.tsx | 32 + .../ElicitationFormPanel.stories.tsx | 66 + .../ElicitationFormPanel.tsx | 43 + .../ElicitationUrlPanel.stories.tsx | 39 + .../ElicitationUrlPanel.tsx | 60 + .../ExperimentalFeaturesPanel.stories.tsx | 118 + .../ExperimentalFeaturesPanel.tsx | 207 + .../HistoryEntry/HistoryEntry.stories.tsx | 108 + .../molecules/HistoryEntry/HistoryEntry.tsx | 109 + .../ImportServerJsonPanel.stories.tsx | 140 + .../ImportServerJsonPanel.tsx | 147 + .../InlineElicitationRequest.stories.tsx | 50 + .../InlineElicitationRequest.tsx | 80 + .../InlineSamplingRequest.stories.tsx | 40 + .../InlineSamplingRequest.tsx | 69 + .../LogControls/LogControls.stories.tsx | 70 + .../molecules/LogControls/LogControls.tsx | 96 + .../PendingClientRequests.stories.tsx | 95 + .../PendingClientRequests.tsx | 22 + .../PromptArgumentsForm.stories.tsx | 71 + .../PromptArgumentsForm.tsx | 74 + .../PromptMessagesDisplay.stories.tsx | 82 + .../PromptMessagesDisplay.tsx | 39 + .../ResourceListItem.stories.tsx | 65 + .../ResourceListItem/ResourceListItem.tsx | 47 + .../ResourcePreviewPanel.stories.tsx | 68 + .../ResourcePreviewPanel.tsx | 77 + .../ResourceTemplateInput.stories.tsx | 39 + .../ResourceTemplateInput.tsx | 67 + .../ResultPanel/ResultPanel.stories.tsx | 71 + .../molecules/ResultPanel/ResultPanel.tsx | 59 + .../RootsTable/RootsTable.stories.tsx | 63 + .../molecules/RootsTable/RootsTable.tsx | 108 + .../SamplingRequestPanel.stories.tsx | 108 + .../SamplingRequestPanel.tsx | 178 + .../SchemaForm/SchemaForm.stories.tsx | 182 + .../molecules/SchemaForm/SchemaForm.tsx | 225 + .../ServerCard/ServerCard.stories.tsx | 101 + .../molecules/ServerCard/ServerCard.tsx | 154 + .../ServerInfoContent.stories.tsx | 96 + .../ServerInfoContent/ServerInfoContent.tsx | 108 + .../ServerSettingsForm.stories.tsx | 77 + .../ServerSettingsForm/ServerSettingsForm.tsx | 200 + .../molecules/TaskCard/TaskCard.stories.tsx | 94 + .../molecules/TaskCard/TaskCard.tsx | 118 + .../ToolDetailPanel.stories.tsx | 96 + .../ToolDetailPanel/ToolDetailPanel.tsx | 102 + .../ToolListItem/ToolListItem.stories.tsx | 59 + .../molecules/ToolListItem/ToolListItem.tsx | 42 + clients/web/src/main.tsx | 8 +- clients/web/src/theme/ActionIcon.ts | 8 + clients/web/src/theme/Alert.ts | 7 + clients/web/src/theme/AppShell.ts | 7 + clients/web/src/theme/Badge.ts | 8 + clients/web/src/theme/Button.ts | 7 + clients/web/src/theme/Card.ts | 9 + clients/web/src/theme/Code.ts | 9 + clients/web/src/theme/Paper.ts | 22 + clients/web/src/theme/Select.ts | 7 + clients/web/src/theme/Switch.ts | 7 + clients/web/src/theme/TextInput.ts | 7 + clients/web/src/theme/index.ts | 11 + clients/web/src/theme/theme.ts | 49 + .../visual-test-illustration.mp4 | Bin 105312 -> 0 bytes .../assets/Color-6BZIO3FS-DsgDikCw.js | 1 - .../assets/DocsRenderer-LL677BLK-BYDUOrCP.js | 46 - .../assets/WithTooltip-65CFNBJE-CwbJDiHq.js | 9 - .../storybook-static/assets/axe-TgFz8JLl.js | 20 - .../assets/chunk-242VQQM5-EiyAdkXv.js | 1 - .../assets/chunk-3LY4VQVK-Di70sXb8.js | 6 - .../storybook-static/assets/chunk-BneVvdWh.js | 1 - .../assets/chunk-CYSK6WYR-BwloUEI2.js | 1 - .../assets/chunk-YKABRMAI-qYi2mBo7.js | 18 - .../assets/components-2tGmqkZv.js | 1 - .../assets/components-BJM7pUDn.js | 92 - .../storybook-static/assets/csf-w9PT11_Q.js | 159 - .../assets/formatter-EIJCOSYU-B7HhblV0.js | 1 - .../assets/iframe-BM0lJmj9.js | 1114 - .../assets/iframe-CKg0vgt5.css | 1 - .../assets/matchers-5TDFFDYO-DhLEHMH_.js | 28 - .../assets/preload-helper-nVtPd9zc.js | 1 - .../assets/react-18-BQs_Gmad.js | 9 - .../assets/react-18-TPsEcK46.js | 1 - .../storybook-static/assets/react-BPg1mOmN.js | 1 - .../assets/react-dom-P9SK4Tis.js | 1 - .../syntaxhighlighter-ED5Y7EFY-BG_DaHvm.js | 6 - .../assets/theming-CFy5g9Ag.js | 39 - .../assets/theming-CUKogAd8.js | 1 - .../web/storybook-static/favicon-wrapper.svg | 46 - clients/web/storybook-static/favicon.svg | 1 - clients/web/storybook-static/icons.svg | 24 - clients/web/storybook-static/iframe.html | 708 - clients/web/storybook-static/index.html | 156 - clients/web/storybook-static/index.json | 1 - .../nunito-sans-bold-italic.woff2 | Bin 49460 -> 0 bytes .../storybook-static/nunito-sans-bold.woff2 | Bin 47144 -> 0 bytes .../storybook-static/nunito-sans-italic.woff2 | Bin 49620 -> 0 bytes .../nunito-sans-regular.woff2 | Bin 47072 -> 0 bytes clients/web/storybook-static/project.json | 1 - .../sb-addons/a11y-3/manager-bundle.js | 57 - .../manager-bundle.js | 352 - .../manager-bundle.js.LEGAL.txt | 40 - .../sb-addons/docs-4/manager-bundle.js | 3 - .../sb-addons/onboarding-5/manager-bundle.js | 97 - .../common-manager-bundle.js | 628 - .../sb-addons/vitest-2/manager-bundle.js | 3 - .../sb-common-assets/favicon-wrapper.svg | 46 - .../sb-common-assets/favicon.svg | 1 - .../nunito-sans-bold-italic.woff2 | Bin 49460 -> 0 bytes .../sb-common-assets/nunito-sans-bold.woff2 | Bin 47144 -> 0 bytes .../sb-common-assets/nunito-sans-italic.woff2 | Bin 49620 -> 0 bytes .../nunito-sans-regular.woff2 | Bin 47072 -> 0 bytes .../sb-manager/globals-runtime.js | 77956 ---------------- .../storybook-static/sb-manager/globals.js | 24 - .../sb-manager/manager-stores.js | 23 - .../storybook-static/sb-manager/runtime.js | 20422 ---- .../vite-inject-mocker-entry.js | 2 - clients/web/tsconfig.app.json | 3 +- 151 files changed, 5895 insertions(+), 102158 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 clients/web/src/App.css create mode 100644 clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.stories.tsx create mode 100644 clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.tsx create mode 100644 clients/web/src/components/atoms/CapabilityItem/CapabilityItem.stories.tsx create mode 100644 clients/web/src/components/atoms/CapabilityItem/CapabilityItem.tsx create mode 100644 clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.stories.tsx create mode 100644 clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.tsx create mode 100644 clients/web/src/components/atoms/ContentViewer/ContentViewer.stories.tsx create mode 100644 clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx create mode 100644 clients/web/src/components/atoms/CopyButton/CopyButton.stories.tsx create mode 100644 clients/web/src/components/atoms/CopyButton/CopyButton.tsx create mode 100644 clients/web/src/components/atoms/InlineError/InlineError.stories.tsx create mode 100644 clients/web/src/components/atoms/InlineError/InlineError.tsx create mode 100644 clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.stories.tsx create mode 100644 clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx create mode 100644 clients/web/src/components/atoms/LogEntry/LogEntry.stories.tsx create mode 100644 clients/web/src/components/atoms/LogEntry/LogEntry.tsx create mode 100644 clients/web/src/components/atoms/MessageBubble/MessageBubble.stories.tsx create mode 100644 clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx create mode 100644 clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.stories.tsx create mode 100644 clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx create mode 100644 clients/web/src/components/atoms/StatusIndicator/StatusIndicator.stories.tsx create mode 100644 clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx create mode 100644 clients/web/src/components/atoms/TransportBadge/TransportBadge.stories.tsx create mode 100644 clients/web/src/components/atoms/TransportBadge/TransportBadge.tsx create mode 100644 clients/web/src/components/molecules/AddServerMenu/AddServerMenu.stories.tsx create mode 100644 clients/web/src/components/molecules/AddServerMenu/AddServerMenu.tsx create mode 100644 clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.tsx create mode 100644 clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.tsx create mode 100644 clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx create mode 100644 clients/web/src/components/molecules/HistoryEntry/HistoryEntry.stories.tsx create mode 100644 clients/web/src/components/molecules/HistoryEntry/HistoryEntry.tsx create mode 100644 clients/web/src/components/molecules/ImportServerJsonPanel/ImportServerJsonPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ImportServerJsonPanel/ImportServerJsonPanel.tsx create mode 100644 clients/web/src/components/molecules/InlineElicitationRequest/InlineElicitationRequest.stories.tsx create mode 100644 clients/web/src/components/molecules/InlineElicitationRequest/InlineElicitationRequest.tsx create mode 100644 clients/web/src/components/molecules/InlineSamplingRequest/InlineSamplingRequest.stories.tsx create mode 100644 clients/web/src/components/molecules/InlineSamplingRequest/InlineSamplingRequest.tsx create mode 100644 clients/web/src/components/molecules/LogControls/LogControls.stories.tsx create mode 100644 clients/web/src/components/molecules/LogControls/LogControls.tsx create mode 100644 clients/web/src/components/molecules/PendingClientRequests/PendingClientRequests.stories.tsx create mode 100644 clients/web/src/components/molecules/PendingClientRequests/PendingClientRequests.tsx create mode 100644 clients/web/src/components/molecules/PromptArgumentsForm/PromptArgumentsForm.stories.tsx create mode 100644 clients/web/src/components/molecules/PromptArgumentsForm/PromptArgumentsForm.tsx create mode 100644 clients/web/src/components/molecules/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx create mode 100644 clients/web/src/components/molecules/PromptMessagesDisplay/PromptMessagesDisplay.tsx create mode 100644 clients/web/src/components/molecules/ResourceListItem/ResourceListItem.stories.tsx create mode 100644 clients/web/src/components/molecules/ResourceListItem/ResourceListItem.tsx create mode 100644 clients/web/src/components/molecules/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ResourcePreviewPanel/ResourcePreviewPanel.tsx create mode 100644 clients/web/src/components/molecules/ResourceTemplateInput/ResourceTemplateInput.stories.tsx create mode 100644 clients/web/src/components/molecules/ResourceTemplateInput/ResourceTemplateInput.tsx create mode 100644 clients/web/src/components/molecules/ResultPanel/ResultPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ResultPanel/ResultPanel.tsx create mode 100644 clients/web/src/components/molecules/RootsTable/RootsTable.stories.tsx create mode 100644 clients/web/src/components/molecules/RootsTable/RootsTable.tsx create mode 100644 clients/web/src/components/molecules/SamplingRequestPanel/SamplingRequestPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/SamplingRequestPanel/SamplingRequestPanel.tsx create mode 100644 clients/web/src/components/molecules/SchemaForm/SchemaForm.stories.tsx create mode 100644 clients/web/src/components/molecules/SchemaForm/SchemaForm.tsx create mode 100644 clients/web/src/components/molecules/ServerCard/ServerCard.stories.tsx create mode 100644 clients/web/src/components/molecules/ServerCard/ServerCard.tsx create mode 100644 clients/web/src/components/molecules/ServerInfoContent/ServerInfoContent.stories.tsx create mode 100644 clients/web/src/components/molecules/ServerInfoContent/ServerInfoContent.tsx create mode 100644 clients/web/src/components/molecules/ServerSettingsForm/ServerSettingsForm.stories.tsx create mode 100644 clients/web/src/components/molecules/ServerSettingsForm/ServerSettingsForm.tsx create mode 100644 clients/web/src/components/molecules/TaskCard/TaskCard.stories.tsx create mode 100644 clients/web/src/components/molecules/TaskCard/TaskCard.tsx create mode 100644 clients/web/src/components/molecules/ToolDetailPanel/ToolDetailPanel.stories.tsx create mode 100644 clients/web/src/components/molecules/ToolDetailPanel/ToolDetailPanel.tsx create mode 100644 clients/web/src/components/molecules/ToolListItem/ToolListItem.stories.tsx create mode 100644 clients/web/src/components/molecules/ToolListItem/ToolListItem.tsx create mode 100644 clients/web/src/theme/ActionIcon.ts create mode 100644 clients/web/src/theme/Alert.ts create mode 100644 clients/web/src/theme/AppShell.ts create mode 100644 clients/web/src/theme/Badge.ts create mode 100644 clients/web/src/theme/Button.ts create mode 100644 clients/web/src/theme/Card.ts create mode 100644 clients/web/src/theme/Code.ts create mode 100644 clients/web/src/theme/Paper.ts create mode 100644 clients/web/src/theme/Select.ts create mode 100644 clients/web/src/theme/Switch.ts create mode 100644 clients/web/src/theme/TextInput.ts create mode 100644 clients/web/src/theme/index.ts create mode 100644 clients/web/src/theme/theme.ts delete mode 100644 clients/web/storybook-static/addon-visual-tests-assets/visual-test-illustration.mp4 delete mode 100644 clients/web/storybook-static/assets/Color-6BZIO3FS-DsgDikCw.js delete mode 100644 clients/web/storybook-static/assets/DocsRenderer-LL677BLK-BYDUOrCP.js delete mode 100644 clients/web/storybook-static/assets/WithTooltip-65CFNBJE-CwbJDiHq.js delete mode 100644 clients/web/storybook-static/assets/axe-TgFz8JLl.js delete mode 100644 clients/web/storybook-static/assets/chunk-242VQQM5-EiyAdkXv.js delete mode 100644 clients/web/storybook-static/assets/chunk-3LY4VQVK-Di70sXb8.js delete mode 100644 clients/web/storybook-static/assets/chunk-BneVvdWh.js delete mode 100644 clients/web/storybook-static/assets/chunk-CYSK6WYR-BwloUEI2.js delete mode 100644 clients/web/storybook-static/assets/chunk-YKABRMAI-qYi2mBo7.js delete mode 100644 clients/web/storybook-static/assets/components-2tGmqkZv.js delete mode 100644 clients/web/storybook-static/assets/components-BJM7pUDn.js delete mode 100644 clients/web/storybook-static/assets/csf-w9PT11_Q.js delete mode 100644 clients/web/storybook-static/assets/formatter-EIJCOSYU-B7HhblV0.js delete mode 100644 clients/web/storybook-static/assets/iframe-BM0lJmj9.js delete mode 100644 clients/web/storybook-static/assets/iframe-CKg0vgt5.css delete mode 100644 clients/web/storybook-static/assets/matchers-5TDFFDYO-DhLEHMH_.js delete mode 100644 clients/web/storybook-static/assets/preload-helper-nVtPd9zc.js delete mode 100644 clients/web/storybook-static/assets/react-18-BQs_Gmad.js delete mode 100644 clients/web/storybook-static/assets/react-18-TPsEcK46.js delete mode 100644 clients/web/storybook-static/assets/react-BPg1mOmN.js delete mode 100644 clients/web/storybook-static/assets/react-dom-P9SK4Tis.js delete mode 100644 clients/web/storybook-static/assets/syntaxhighlighter-ED5Y7EFY-BG_DaHvm.js delete mode 100644 clients/web/storybook-static/assets/theming-CFy5g9Ag.js delete mode 100644 clients/web/storybook-static/assets/theming-CUKogAd8.js delete mode 100644 clients/web/storybook-static/favicon-wrapper.svg delete mode 100644 clients/web/storybook-static/favicon.svg delete mode 100644 clients/web/storybook-static/icons.svg delete mode 100644 clients/web/storybook-static/iframe.html delete mode 100644 clients/web/storybook-static/index.html delete mode 100644 clients/web/storybook-static/index.json delete mode 100644 clients/web/storybook-static/nunito-sans-bold-italic.woff2 delete mode 100644 clients/web/storybook-static/nunito-sans-bold.woff2 delete mode 100644 clients/web/storybook-static/nunito-sans-italic.woff2 delete mode 100644 clients/web/storybook-static/nunito-sans-regular.woff2 delete mode 100644 clients/web/storybook-static/project.json delete mode 100644 clients/web/storybook-static/sb-addons/a11y-3/manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-addons/chromatic-com-storybook-1/manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-addons/chromatic-com-storybook-1/manager-bundle.js.LEGAL.txt delete mode 100644 clients/web/storybook-static/sb-addons/docs-4/manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-addons/onboarding-5/manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-addons/vitest-2/manager-bundle.js delete mode 100644 clients/web/storybook-static/sb-common-assets/favicon-wrapper.svg delete mode 100644 clients/web/storybook-static/sb-common-assets/favicon.svg delete mode 100644 clients/web/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 delete mode 100644 clients/web/storybook-static/sb-common-assets/nunito-sans-bold.woff2 delete mode 100644 clients/web/storybook-static/sb-common-assets/nunito-sans-italic.woff2 delete mode 100644 clients/web/storybook-static/sb-common-assets/nunito-sans-regular.woff2 delete mode 100644 clients/web/storybook-static/sb-manager/globals-runtime.js delete mode 100644 clients/web/storybook-static/sb-manager/globals.js delete mode 100644 clients/web/storybook-static/sb-manager/manager-stores.js delete mode 100644 clients/web/storybook-static/sb-manager/runtime.js delete mode 100644 clients/web/storybook-static/vite-inject-mocker-entry.js diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..570fc6bb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,159 @@ +# Forenzo + +Forensic interview video platform for Children's Advocacy Centers (CACs). Upload forensic interview recordings, automatically transcribe with speaker diarization, generate AI review indices with timestamped key moments, and share securely with law enforcement, prosecutors, and MDT members via PIN-protected magic links. + +## Maintenance Rules + +### Keep documentation files up to date +- When adding, removing, renaming, or changing the purpose of any file or folder, update the corresponding entry in the README.md +- When the structure of the project, the tech stack, or the developer setup changes, update README.md with the details. +- When adding new commands, dependencies, or architectural patterns, update the relevant sections of README.md as well. +- When rules for implementation and testing change, update this file AGENTS.md + +### CJIS session timeout - background polling rules +- CJIS 5.5.5 requires a 30-minute inactivity timeout. Background pollers (setInterval) must NEVER refresh the session. +- Any `setInterval` or recurring fetch that runs without user interaction MUST use `backgroundFetch` (from `src/utils/api.ts`), NOT `authFetch` or raw `fetch`. +- `backgroundFetch` adds an `X-Background-Poll: true` header. The server sees this and authenticates WITHOUT extending the session expiry (via Better Auth's `disableRefresh: true`). +- The ONLY pollers that should use regular `authFetch` are ones tied to active video playback (session heartbeat, watch progress) - because watching a video IS user activity. +- If you add a new poller or setInterval that hits an authenticated endpoint, use `backgroundFetch`. If you use `authFetch` instead, the 30-minute idle timeout breaks and CJIS compliance is violated. +- The CORS config in `server/index.ts` reflects whatever headers the client requests (no explicit allowlist). No special CORS setup needed for new custom headers. + +### Always test new or modified code +- Ensure all code has corresponding tests +- Ensure test coverage for each file is at least 90% +- In unit tests that expect error output, suppress it from the console + +### Responding to Code Reviews +- When asked to respond to a code review of a PR, + - it is not necessary to implement all suggestions + - you are free to implement suggestions in a different way or to ignore if there is a good reason + - after making the changes, respond to each review comment with what was done (or why it was ignored) + +### Lint-fixed, Formatted code +- Ensure linting and formatting are applied after every change +- ALWAYS do `npm run format`, `npm run lint`, `npm run typecheck`, `npm run test` and `npm run build` before pushing any changes + +### Typescript instructions +- Use TypeScript for all new code +- Follow TypeScript best practices and coding standards +- NEVER use 'any' as a type +- NEVER suppress error types (e.g., no-unused-vars, no-explicit-any) in the typescript or eslint configuration as a way of satisfying the linter or compiler. +- Utilize type annotations and interfaces to improve code clarity and maintainability +- Leverage TypeScript's type inference and static analysis features for better code quality and refactoring +- Use type guards and type assertions to handle potential type mismatches and ensure type safety +- Take advantage of TypeScript's advanced features like generics, type aliases, and conditional types to write more expressive and reusable code +- Regularly review and refactor TypeScript code to ensure it remains well-structured and adheres to evolving best practices + +## React instructions +- UI Components + - We are using the Mantine component library for UI. + - Instructions are at https://mantine.dev/llms.txt + - Avoid using div and other basic HTML elements for layout purposes. + - Prefer Mantine's Box, Group, and Stack components for layout. + - Use Mantine's theme and styling utilities to ensure a consistent and responsive design. + - NEVER use inline styles on a component. + - NEVER use raw hex values (`#ddd`, `#94a3b8`, etc.) or `rgba()` literals for colors in component props or theme files. Use `--forenzo-*` CSS custom properties defined in `App.css :root` (e.g., `c: 'var(--forenzo-text-primary)'`). If no existing token fits, add one to `:root` first. + - NEVER add a CSS class to a Mantine component when the styles can instead be expressed as component props or a theme variant. CSS classes are a last resort. + - PREFER component props (via `.withProps()`) to CSS for behavioral and visual styles. + - PREFER defining styles as theme variants (via `Component.extend()` in `src/theme/.ts`) over CSS classes. Each Mantine component with custom variants has its own file in `src/theme/`, exporting a `Theme` constant. The barrel `src/theme/index.ts` re-exports them all and `theme.ts` imports from the barrel. Flat CSS properties (margin, padding, background, border, color, font-size, etc.) belong in the theme. Only pseudo-selectors, nested child selectors, keyframes, and native HTML element styles belong in App.css. + - App.css must contain ONLY styles that cannot be expressed in the Mantine theme: `@keyframes`, pseudo-selectors (`:hover`, `:focus`), cross-component hover relationships, nested child-element selectors for third-party HTML output (e.g. ReactMarkdown), and styles for native HTML elements (`img`, `iframe`). When refactoring a component, actively move any flat CSS properties out of App.css and into theme variants or `.withProps()` constants. + - NEVER use inline code; instead extract to functions in the same file, exported or located in a shared location if immediately reusable. + - In a component's file, for sub-components: + - ALWAYS use Mantine components for layout and content, configured with props for styling and behavior. + - ALWAYS declare a subcomponent as a named constant using `.withProps()` if it has two or more props. + - NEVER use `Box` for subcomponent constants — `Box` does not support `.withProps()`. Use `Group`, `Stack`, `Flex`, `Text`, `Paper`, `UnstyledButton`, or `Image` instead. Pick the component that best matches the purpose: `Paper` for bordered/surfaced containers, `Text` for any text or content wrapper, `Stack`/`Group`/`Flex` for layout. + - NEVER use a CSS class on a subcomponent constant when the styles can be expressed as a Mantine theme variant instead. Define variants in `src/theme/.ts` using `Component.extend({ styles: (_theme, props) => { ... } })` and reference them with `variant="variantName"` on the component or in `.withProps()`. + - CSS classes are ONLY acceptable on subcomponents for styles that cannot be expressed as flat CSS-in-JS properties in the theme — specifically: pseudo-selectors (`:hover`, `:focus`), cross-component hover relationships (`.parent:hover .child`), nested child-element selectors (`.wrapper p`, `.wrapper code`), `@keyframes` definitions, and native HTML elements (`img`, `iframe`) that are not Mantine components. + - When a theme variant needs a CSS class for nested/pseudo selectors, use `classNames` in the theme extension to auto-assign it — never add `className` manually in JSX for theme-styled components. + - Example — subcomponent constant with `withProps`: + ```tsx + const CardContent = Group.withProps({ + flex: 1, + align: 'flex-start', + justify: 'space-between', + wrap: 'nowrap', + }); + return ... + ``` + - Example — theme variant with auto-assigned className for nested selectors: + ```tsx + // src/theme/Paper.ts + export const ThemePaper = Paper.extend({ + classNames: (_theme, props) => { + if (props.variant === 'message') return { root: 'message' }; + return {}; + }, + styles: (_theme, props) => { + if (props.variant === 'message') { + return { root: { padding: '1.5rem', borderRadius: 12 } }; + } + return { root: {} }; + }, + }), + + // Component.tsx + const MessageContainer = Paper.withProps({ variant: 'message' }); + ``` + +### Database & Prisma instructions + +#### Schema management +- The database schema is managed by [Prisma ORM](https://www.prisma.io/). The schema is defined in `prisma/schema.prisma`. +- To change the schema (add/remove tables, columns, enums, etc.): + 1. Edit `prisma/schema.prisma` + 2. Run `npm run db:migrate` to generate and apply a migration + 3. Prisma auto-generates the migration SQL in `prisma/migrations/` +- NEVER edit migration files in `prisma/migrations/` by hand — they are auto-generated. +- NEVER write raw SQL for schema changes — use Prisma Migrate. + +#### Two database pools +- **Prisma client** (`server/lib/prisma.ts`) — all application queries. Import as `import { prisma } from '../lib/prisma'`. +- **Better Auth pg Pool** (`server/lib/auth.ts`, 5 connections) — Better Auth's own pool for session/user management. Do NOT touch this pool in application code. +- Better Auth tables (User, Session, Account, Organization, Member, etc.) are in the Prisma schema for type safety and can be READ via Prisma, but Better Auth manages their lifecycle. + +#### Writing queries +- Prefer Prisma's typed query builder over raw SQL for all new queries. +- Import the client: `import { prisma } from '../lib/prisma'` +- Import types when needed: `import { Prisma } from '@prisma/client'` or `import type { CaseStatus } from '@prisma/client'` +- Model names use PascalCase: `Case`, `Video`, `Transcript`, `TranscriptSegment`, `AiSummary`, `Share`, `ProcessingJob`, `VideoTag`, `ApiKey`, `Billing`, `TagCategory`, `TagPreferences`, `MultipartUpload`, `McpToken`, `PinRateLimit`, `TranscriptCorrection`, `TranscriptReview`, `TranscriptQaCache`, `ReviewAttestation`, `OrgSettings` +- Field names use camelCase (mapped to snake_case columns via `@map`): `caseNumber`, `childFirstName`, `videoId`, `fileSizeBytes`, `storagePath`, `speakerNames`, etc. + +#### Type conversions in responses +- `BigInt` fields (e.g., `fileSizeBytes`): use `Number()` before JSON serialization +- `Decimal` fields (e.g., `durationSecs`, `confidence`): use `Number()` before JSON serialization +- `Json` fields (e.g., `metadata`, `speakerNames`, `permissions`): cast with `as Record` or appropriate type +- Prisma enums (e.g., `CaseStatus`, `ProcessingStatus`): use directly, they serialize as strings + +#### Transactions +- Use `prisma.$transaction(async (tx) => { ... })` for multi-step operations +- Inside the transaction callback, use `tx.model.method()` instead of `prisma.model.method()` +- Raw SQL inside transactions: `tx.$queryRaw` / `tx.$executeRaw` + +#### Raw SQL — last resort only +- The goal is to minimize raw SQL in TypeScript. Always try Prisma's query builder first. +- `$queryRaw` — returns rows. Use for SELECT queries that Prisma can't express. +- `$executeRaw` — returns affected row count. Use for INSERT/UPDATE/DELETE that Prisma can't express. +- Raw SQL is acceptable ONLY for these specific PostgreSQL features: + - **`access_log` table** — partitioned, not modeled in Prisma. All reads/writes must be raw. + - **Full-text search** — `websearch_to_tsquery`, `ts_headline`, `ts_rank` + - **Advisory locks** — `pg_try_advisory_xact_lock` + - **Atomic UPDATE ... RETURNING** with conditional WHERE (check-and-act in one statement) + - **`make_interval()`** — PostgreSQL interval construction from numeric values + - **LATERAL JOIN** — correlated subqueries in FROM clause + - **SELECT ... FOR UPDATE** — row-level locking +- If you find yourself writing raw SQL for anything else (simple CRUD, JOINs, aggregations, filters), stop and use the Prisma query builder instead. +- Always use tagged template literals for safe parameterization: + ```ts + prisma.$queryRaw`SELECT ... WHERE id = ${someVar}` + ``` +- NEVER use `prisma.$queryRawUnsafe` — SQL injection risk. +- Type casts required in raw SQL (Prisma tagged templates send JS types, PostgreSQL won't auto-coerce): + - `${id}::uuid` for UUID columns (but NOT for `access_log.resource_id` — it's TEXT) + - `${ip}::inet` for inet columns + - `${JSON.stringify(obj)}::jsonb` for jsonb columns + - `${value}::access_action` for enum columns + +#### Queries involving Better Auth user lookups +- No FK relation exists from application tables to the `user` table (deliberate — Better Auth manages those tables) +- For user name lookups, do a separate query: `prisma.user.findUnique({ where: { id }, select: { name: true } })` +- For batch lookups: `prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, name: true } })` and build a Map diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1e49e1d0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +@./AGENTS.md +@./README.md diff --git a/clients/web/.gitignore b/clients/web/.gitignore index a547bf36..87b58f06 100644 --- a/clients/web/.gitignore +++ b/clients/web/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +storybook-static *.local # Editor directories and files diff --git a/clients/web/.storybook/preview.tsx b/clients/web/.storybook/preview.tsx index 1274c85a..26fa9dcd 100644 --- a/clients/web/.storybook/preview.tsx +++ b/clients/web/.storybook/preview.tsx @@ -1,12 +1,16 @@ import type { Preview } from '@storybook/react-vite' import { MantineProvider } from '@mantine/core' +import { Notifications } from '@mantine/notifications' import '@mantine/core/styles.css' import '@mantine/notifications/styles.css' +import '../src/App.css' +import { theme } from '../src/theme/theme' const preview: Preview = { decorators: [ (Story) => ( - + + ), @@ -21,6 +25,7 @@ const preview: Preview = { a11y: { test: 'todo', }, + layout: 'centered', }, } diff --git a/clients/web/eslint.config.js b/clients/web/eslint.config.js index 5964a385..3f5c7c2f 100644 --- a/clients/web/eslint.config.js +++ b/clients/web/eslint.config.js @@ -8,7 +8,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' -export default defineConfig([globalIgnores(['dist']), { +export default defineConfig([globalIgnores(['dist', 'storybook-static']), { files: ['**/*.{ts,tsx}'], extends: [ js.configs.recommended, diff --git a/clients/web/index.html b/clients/web/index.html index 5e3836a6..ae1890ac 100644 --- a/clients/web/index.html +++ b/clients/web/index.html @@ -4,7 +4,7 @@ - web + MCP Inspector
diff --git a/clients/web/src/App.css b/clients/web/src/App.css new file mode 100644 index 00000000..290cc47e --- /dev/null +++ b/clients/web/src/App.css @@ -0,0 +1,105 @@ +/* + * App.css — Global styles that CANNOT be expressed in the Mantine theme. + * + * Only the following belong here: + * - :root CSS custom properties (--inspector-* design tokens) + * - @keyframes animations + * - Pseudo-selectors (:hover, :focus) that cross component boundaries + * - Nested child-element selectors for third-party HTML output + * - Styles for native HTML elements (img, iframe, etc.) + * + * Everything else (margin, padding, background, border, color, font-size) + * belongs in the Mantine theme via Component.extend() or .withProps(). + */ + +:root { + /* ── Brand ─────────────────────────────────────────────── */ + --inspector-brand-primary: var(--mantine-primary-color-filled); + + /* ── Text ──────────────────────────────────────────────── */ + --inspector-text-primary: var(--mantine-color-text); + --inspector-text-secondary: var(--mantine-color-dimmed); + --inspector-text-inverse: var(--mantine-color-white); + + /* ── Surfaces ──────────────────────────────────────────── */ + --inspector-surface-body: var(--mantine-color-body); + --inspector-surface-card: var(--mantine-color-default); + --inspector-surface-code: var(--mantine-color-gray-0); + + /* ── Borders ───────────────────────────────────────────── */ + --inspector-border-default: var(--mantine-color-default-border); + --inspector-border-subtle: var(--mantine-color-gray-3); + + /* ── Connection status ─────────────────────────────────── */ + --inspector-status-connected: var(--mantine-color-green-6); + --inspector-status-connecting: var(--mantine-color-yellow-6); + --inspector-status-disconnected: var(--mantine-color-gray-5); + --inspector-status-failed: var(--mantine-color-red-6); + + /* ── Log levels (RFC 5424) ─────────────────────────────── */ + --inspector-log-debug: var(--mantine-color-gray-6); + --inspector-log-info: var(--mantine-color-blue-6); + --inspector-log-notice: var(--mantine-color-teal-6); + --inspector-log-warning: var(--mantine-color-yellow-6); + --inspector-log-error: var(--mantine-color-red-6); + --inspector-log-critical: var(--mantine-color-red-8); + --inspector-log-alert: var(--mantine-color-red-9); + --inspector-log-emergency: var(--mantine-color-red-9); + + /* ── Annotations ───────────────────────────────────────── */ + --inspector-annotation-destructive: var(--mantine-color-red-6); + --inspector-annotation-long-run: var(--mantine-color-yellow-6); + --inspector-annotation-read-only: var(--mantine-color-dark-6); + --inspector-annotation-audience: var(--mantine-color-gray-6); + --inspector-annotation-priority-high: var(--mantine-color-orange-6); + --inspector-annotation-priority-medium: var(--mantine-color-yellow-6); + --inspector-annotation-priority-low: var(--mantine-color-green-6); + + /* ── Task status ───────────────────────────────────────── */ + --inspector-task-waiting: var(--mantine-color-gray-5); + --inspector-task-running: var(--mantine-color-blue-6); + --inspector-task-completed: var(--mantine-color-green-6); + --inspector-task-failed: var(--mantine-color-red-6); + --inspector-task-cancelled: var(--mantine-color-yellow-6); + + /* ── History ───────────────────────────────────────────── */ + --inspector-history-success: var(--mantine-color-green-6); + --inspector-history-error: var(--mantine-color-red-6); +} + +/* Dark mode overrides — tokens that need adjustment in dark scheme */ +[data-mantine-color-scheme='dark'] { + --inspector-surface-code: var(--mantine-color-dark-6); + --inspector-border-subtle: var(--mantine-color-dark-4); +} + +/* ── Animations ──────────────────────────────────────────── */ + +@keyframes inspector-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} + +@keyframes inspector-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* ── Status dot animations ───────────────────────────────── */ + +.inspector-pulse { + animation: inspector-pulse 1.5s ease-in-out infinite; +} + +.inspector-spin { + animation: inspector-spin 1s linear infinite; +} diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index e1c70a0e..f636eb58 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -1,11 +1,39 @@ -import { Title, Text, Container, Stack } from '@mantine/core' +import { + Title, + Text, + Container, + Stack, + useMantineColorScheme, + useComputedColorScheme, + ActionIcon, + Group, +} from '@mantine/core' function App() { + const { setColorScheme } = useMantineColorScheme() + const computedColorScheme = useComputedColorScheme('light') + + const toggleColorScheme = () => { + setColorScheme(computedColorScheme === 'dark' ? 'light' : 'dark') + } + return ( - MCP Inspector - Web client for the Model Context Protocol Inspector + + MCP Inspector + + {computedColorScheme === 'dark' ? '\u2600' : '\u263E'} + + + + Web client for the Model Context Protocol Inspector + ) diff --git a/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.stories.tsx b/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.stories.tsx new file mode 100644 index 00000000..0121aa2d --- /dev/null +++ b/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { AnnotationBadge } from './AnnotationBadge' + +const meta: Meta = { + title: 'Atoms/AnnotationBadge', + component: AnnotationBadge, +} + +export default meta +type Story = StoryObj + +export const Audience: Story = { + args: { + label: 'user', + variant: 'audience', + }, +} + +export const ReadOnly: Story = { + args: { + label: 'read-only', + variant: 'readOnly', + }, +} + +export const Destructive: Story = { + args: { + label: 'destructive', + variant: 'destructive', + }, +} + +export const LongRun: Story = { + args: { + label: 'long-run', + variant: 'longRun', + }, +} + +export const PriorityHigh: Story = { + args: { + label: 'priority: high', + variant: 'priority', + }, +} + +export const Default: Story = { + args: { + label: 'custom', + variant: 'default', + }, +} diff --git a/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.tsx b/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.tsx new file mode 100644 index 00000000..1b8fc159 --- /dev/null +++ b/clients/web/src/components/atoms/AnnotationBadge/AnnotationBadge.tsx @@ -0,0 +1,27 @@ +import { Badge } from '@mantine/core' + +export type AnnotationVariant = 'audience' | 'readOnly' | 'destructive' | 'longRun' | 'priority' | 'default' + +export interface AnnotationBadgeProps { + label: string + variant?: AnnotationVariant +} + +const variantMap: Record = { + audience: { color: 'gray', variant: 'outline' }, + readOnly: { color: 'dark', variant: 'filled' }, + destructive: { color: 'red', variant: 'outline' }, + longRun: { color: 'yellow', variant: 'outline' }, + priority: { color: 'orange', variant: 'light' }, + default: { color: 'gray', variant: 'light' }, +} + +export function AnnotationBadge({ label, variant = 'default' }: AnnotationBadgeProps) { + const { color, variant: badgeVariant } = variantMap[variant] + + return ( + + {label} + + ) +} diff --git a/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.stories.tsx b/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.stories.tsx new file mode 100644 index 00000000..c7519033 --- /dev/null +++ b/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { CapabilityItem } from './CapabilityItem'; + +const meta: Meta = { + title: 'Atoms/CapabilityItem', + component: CapabilityItem, +}; + +export default meta; +type Story = StoryObj; + +export const Supported: Story = { + args: { + name: 'Tools', + supported: true, + }, +}; + +export const SupportedWithCount: Story = { + args: { + name: 'Tools', + supported: true, + count: 4, + }, +}; + +export const NotSupported: Story = { + args: { + name: 'Completions', + supported: false, + }, +}; diff --git a/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.tsx b/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.tsx new file mode 100644 index 00000000..cdd7c733 --- /dev/null +++ b/clients/web/src/components/atoms/CapabilityItem/CapabilityItem.tsx @@ -0,0 +1,18 @@ +import { Group, Text } from '@mantine/core'; + +export interface CapabilityItemProps { + name: string; + supported: boolean; + count?: number; +} + +export function CapabilityItem({ name, supported, count }: CapabilityItemProps) { + const label = count != null ? `${name} (${count})` : name; + + return ( + + {supported ? '\u2713' : '\u2717'} + {label} + + ); +} diff --git a/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.stories.tsx b/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.stories.tsx new file mode 100644 index 00000000..faab9443 --- /dev/null +++ b/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { ConnectionToggle } from './ConnectionToggle'; + +const meta: Meta = { + title: 'Atoms/ConnectionToggle', + component: ConnectionToggle, + args: { + onChange: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Connected: Story = { + args: { + checked: true, + loading: false, + disabled: false, + }, +}; + +export const Disconnected: Story = { + args: { + checked: false, + loading: false, + disabled: false, + }, +}; + +export const Loading: Story = { + args: { + checked: false, + loading: true, + disabled: false, + }, +}; + +export const Disabled: Story = { + args: { + checked: false, + loading: false, + disabled: true, + }, +}; diff --git a/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.tsx b/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.tsx new file mode 100644 index 00000000..b01d4281 --- /dev/null +++ b/clients/web/src/components/atoms/ConnectionToggle/ConnectionToggle.tsx @@ -0,0 +1,19 @@ +import { Switch } from '@mantine/core'; + +export interface ConnectionToggleProps { + checked: boolean; + loading: boolean; + disabled: boolean; + onChange: (checked: boolean) => void; +} + +export function ConnectionToggle({ checked, loading, disabled, onChange }: ConnectionToggleProps) { + return ( + onChange(event.currentTarget.checked)} + /> + ); +} diff --git a/clients/web/src/components/atoms/ContentViewer/ContentViewer.stories.tsx b/clients/web/src/components/atoms/ContentViewer/ContentViewer.stories.tsx new file mode 100644 index 00000000..cfa4db20 --- /dev/null +++ b/clients/web/src/components/atoms/ContentViewer/ContentViewer.stories.tsx @@ -0,0 +1,78 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { ContentViewer } from './ContentViewer'; + +const meta: Meta = { + title: 'Atoms/ContentViewer', + component: ContentViewer, +}; + +export default meta; +type Story = StoryObj; + +export const PlainText: Story = { + args: { + type: 'text', + content: 'Hello, world!\nThis is plain text.', + }, +}; + +export const JsonContent: Story = { + args: { + type: 'json', + content: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}', + }, +}; + +export const ImagePreview: Story = { + args: { + type: 'image', + content: + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', + mimeType: 'image/png', + }, +}; + +export const LongContent: Story = { + args: { + type: 'json', + content: JSON.stringify({ + name: 'my-app', + version: '2.5.0', + description: 'A comprehensive application configuration', + author: 'Inspector Team', + license: 'MIT', + repository: 'https://github.com/example/my-app', + dependencies: { + react: '^18.0.0', + typescript: '^5.0.0', + mantine: '^7.0.0', + }, + devDependencies: { + vitest: '^1.0.0', + storybook: '^8.0.0', + eslint: '^9.0.0', + }, + scripts: { + dev: 'vite', + build: 'tsc && vite build', + test: 'vitest', + lint: 'eslint .', + }, + settings: { + theme: 'dark', + language: 'en', + notifications: true, + maxRetries: 3, + }, + }), + }, +}; + +export const WithCopyButton: Story = { + args: { + type: 'text', + content: 'Hello, world!\nThis text can be copied.', + onCopy: fn(), + }, +}; diff --git a/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx b/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx new file mode 100644 index 00000000..22facfbe --- /dev/null +++ b/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx @@ -0,0 +1,43 @@ +import { Button, Code, Image, Stack } from '@mantine/core'; + +export interface ContentViewerProps { + type: 'text' | 'json' | 'image' | 'audio'; + content: string; + mimeType?: string; + onCopy?: () => void; +} + +function formatJson(content: string): string { + try { + return JSON.stringify(JSON.parse(content), null, 2); + } catch { + return content; + } +} + +export function ContentViewer({ type, content, mimeType, onCopy }: ContentViewerProps) { + return ( + + {type === 'json' && {formatJson(content)}} + {type === 'text' && {content}} + {type === 'image' && ( + Content preview + )} + {type === 'audio' && ( + + )} + {onCopy && ( + + )} + + ); +} diff --git a/clients/web/src/components/atoms/CopyButton/CopyButton.stories.tsx b/clients/web/src/components/atoms/CopyButton/CopyButton.stories.tsx new file mode 100644 index 00000000..3b9fe08c --- /dev/null +++ b/clients/web/src/components/atoms/CopyButton/CopyButton.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { CopyButton } from './CopyButton'; + +const meta: Meta = { + title: 'Atoms/CopyButton', + component: CopyButton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 'Hello, world!', + }, +}; + +export const LongValue: Story = { + args: { + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + }, +}; diff --git a/clients/web/src/components/atoms/CopyButton/CopyButton.tsx b/clients/web/src/components/atoms/CopyButton/CopyButton.tsx new file mode 100644 index 00000000..8b0908e7 --- /dev/null +++ b/clients/web/src/components/atoms/CopyButton/CopyButton.tsx @@ -0,0 +1,19 @@ +import { ActionIcon, CopyButton as MantineCopyButton, Tooltip } from '@mantine/core'; + +export interface CopyButtonProps { + value: string; +} + +export function CopyButton({ value }: CopyButtonProps) { + return ( + + {({ copied, copy }) => ( + + + {copied ? '\u2713' : '\u2398'} + + + )} + + ); +} diff --git a/clients/web/src/components/atoms/InlineError/InlineError.stories.tsx b/clients/web/src/components/atoms/InlineError/InlineError.stories.tsx new file mode 100644 index 00000000..3c310d51 --- /dev/null +++ b/clients/web/src/components/atoms/InlineError/InlineError.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { InlineError } from './InlineError'; + +const meta: Meta = { + title: 'Atoms/InlineError', + component: InlineError, +}; + +export default meta; +type Story = StoryObj; + +export const ShortError: Story = { + args: { + message: 'Connection timeout after 20s', + }, +}; + +export const WithRetryCount: Story = { + args: { + message: 'Connection failed', + retryCount: 3, + maxRetries: 5, + }, +}; + +export const WithDetails: Story = { + args: { + message: 'Connection refused', + details: + 'Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)', + }, +}; + +export const WithDocLink: Story = { + args: { + message: 'Connection failed', + docLink: 'https://example.com/troubleshooting', + }, +}; + +export const FullError: Story = { + args: { + message: 'Connection refused', + details: + 'Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)', + retryCount: 3, + maxRetries: 5, + docLink: 'https://example.com/troubleshooting', + }, +}; diff --git a/clients/web/src/components/atoms/InlineError/InlineError.tsx b/clients/web/src/components/atoms/InlineError/InlineError.tsx new file mode 100644 index 00000000..27b394b1 --- /dev/null +++ b/clients/web/src/components/atoms/InlineError/InlineError.tsx @@ -0,0 +1,43 @@ +import { Alert, Anchor, Group, Spoiler, Stack, Text } from '@mantine/core'; + +export interface InlineErrorProps { + message: string; + details?: string; + retryCount?: number; + maxRetries?: number; + onShowMore?: () => void; + docLink?: string; +} + +export function InlineError({ + message, + details, + retryCount, + maxRetries, + docLink, +}: InlineErrorProps) { + return ( + + + {retryCount !== undefined && ( + + Retry attempt {retryCount} + {maxRetries !== undefined && ` of ${maxRetries}`} + + )} + {details && ( + + {details} + + )} + {docLink && ( + + + View Troubleshooting Guide → + + + )} + + + ); +} diff --git a/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.stories.tsx b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.stories.tsx new file mode 100644 index 00000000..1c92cc5a --- /dev/null +++ b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { ListChangedIndicator } from './ListChangedIndicator'; + +const meta: Meta = { + title: 'Atoms/ListChangedIndicator', + component: ListChangedIndicator, +}; + +export default meta; +type Story = StoryObj; + +export const Visible: Story = { + args: { + visible: true, + onRefresh: fn(), + }, +}; + +export const Hidden: Story = { + args: { + visible: false, + onRefresh: fn(), + }, +}; diff --git a/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx new file mode 100644 index 00000000..0c9db765 --- /dev/null +++ b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx @@ -0,0 +1,27 @@ +import { Button, Flex, Group, Text } from '@mantine/core'; + +export interface ListChangedIndicatorProps { + visible: boolean; + onRefresh: () => void; +} + +const Dot = Flex.withProps({ + w: 8, + h: 8, + style: { borderRadius: '50%' }, + bg: 'var(--inspector-status-connecting)', +}); + +export function ListChangedIndicator({ visible, onRefresh }: ListChangedIndicatorProps) { + if (!visible) return null; + + return ( + + + List updated + + + ); +} diff --git a/clients/web/src/components/atoms/LogEntry/LogEntry.stories.tsx b/clients/web/src/components/atoms/LogEntry/LogEntry.stories.tsx new file mode 100644 index 00000000..50eb94e1 --- /dev/null +++ b/clients/web/src/components/atoms/LogEntry/LogEntry.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { LogEntry } from './LogEntry'; + +const meta: Meta = { + title: 'Atoms/LogEntry', + component: LogEntry, +}; + +export default meta; +type Story = StoryObj; + +export const Debug: Story = { + args: { + timestamp: '2026-03-17T10:00:00Z', + level: 'debug', + message: 'Initializing connection pool', + }, +}; + +export const Info: Story = { + args: { + timestamp: '2026-03-17T10:00:01Z', + level: 'info', + message: 'Server started on port 3000', + }, +}; + +export const Notice: Story = { + args: { + timestamp: '2026-03-17T10:00:02Z', + level: 'notice', + message: 'Configuration reloaded successfully', + }, +}; + +export const Warning: Story = { + args: { + timestamp: '2026-03-17T10:00:03Z', + level: 'warning', + message: 'Memory usage above 80%', + }, +}; + +export const Error: Story = { + args: { + timestamp: '2026-03-17T10:00:04Z', + level: 'error', + message: 'Failed to connect to database', + }, +}; + +export const Critical: Story = { + args: { + timestamp: '2026-03-17T10:00:05Z', + level: 'critical', + message: 'Disk space critically low', + }, +}; + +export const Alert: Story = { + args: { + timestamp: '2026-03-17T10:00:06Z', + level: 'alert', + message: 'Service degradation detected', + }, +}; + +export const Emergency: Story = { + args: { + timestamp: '2026-03-17T10:00:07Z', + level: 'emergency', + message: 'System is unusable', + }, +}; + +export const WithLogger: Story = { + args: { + timestamp: '2026-03-17T10:00:08Z', + level: 'info', + message: 'Tool execution completed', + logger: 'mcp-server', + }, +}; diff --git a/clients/web/src/components/atoms/LogEntry/LogEntry.tsx b/clients/web/src/components/atoms/LogEntry/LogEntry.tsx new file mode 100644 index 00000000..289c8b66 --- /dev/null +++ b/clients/web/src/components/atoms/LogEntry/LogEntry.tsx @@ -0,0 +1,72 @@ +import { Badge, Group, Text } from '@mantine/core'; + +export type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; + +export interface LogEntryProps { + timestamp: string; + level: LogLevel; + message: string; + logger?: string; +} + +const levelBadgeColor: Record = { + debug: 'gray', + info: 'blue', + notice: 'teal', + warning: 'yellow', + error: 'red', + critical: 'red', + alert: 'red', + emergency: 'red', +}; + +const levelBadgeVariant: Record = { + debug: 'light', + info: 'light', + notice: 'light', + warning: 'light', + error: 'light', + critical: 'filled', + alert: 'filled', + emergency: 'filled', +}; + +const levelMessageColor: Record = { + debug: 'dimmed', + info: 'blue', + notice: undefined, + warning: 'yellow', + error: 'red', + critical: 'red', + alert: 'red', + emergency: 'red', +}; + +function isBoldLevel(level: LogLevel): boolean { + return level === 'alert' || level === 'emergency'; +} + +export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) { + return ( + + + {timestamp} + + + {level} + + {logger && ( + + [{logger}] + + )} + + {message} + + + ); +} diff --git a/clients/web/src/components/atoms/MessageBubble/MessageBubble.stories.tsx b/clients/web/src/components/atoms/MessageBubble/MessageBubble.stories.tsx new file mode 100644 index 00000000..096cd94f --- /dev/null +++ b/clients/web/src/components/atoms/MessageBubble/MessageBubble.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MessageBubble } from './MessageBubble'; + +const meta: Meta = { + title: 'Atoms/MessageBubble', + component: MessageBubble, +}; + +export default meta; +type Story = StoryObj; + +export const UserText: Story = { + args: { + index: 0, + role: 'user', + content: 'Hello, my name is John and I like cats', + }, +}; + +export const AssistantText: Story = { + args: { + index: 1, + role: 'assistant', + content: + "Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?", + }, +}; + +export const UserWithImage: Story = { + args: { + index: 0, + role: 'user', + content: 'Check this out', + imageContent: { + data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', + mimeType: 'image/png', + }, + }, +}; + +export const LongMessage: Story = { + args: { + index: 0, + role: 'user', + content: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.', + }, +}; diff --git a/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx b/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx new file mode 100644 index 00000000..97ded850 --- /dev/null +++ b/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx @@ -0,0 +1,44 @@ +import { Image, Paper, Stack, Text } from '@mantine/core'; + +export interface MessageBubbleProps { + index: number; + role: 'user' | 'assistant'; + content: string; + imageContent?: { data: string; mimeType: string }; + audioContent?: { data: string; mimeType: string }; +} + +export function MessageBubble({ + index, + role, + content, + imageContent, + audioContent, +}: MessageBubbleProps) { + return ( + + + + [{index}] role: {role} + + "{content}" + {imageContent && ( + + )} + {audioContent && ( + + )} + + + ); +} diff --git a/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.stories.tsx b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.stories.tsx new file mode 100644 index 00000000..b1c4d965 --- /dev/null +++ b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { ProgressDisplay } from './ProgressDisplay'; + +const meta: Meta = { + title: 'Atoms/ProgressDisplay', + component: ProgressDisplay, +}; + +export default meta; +type Story = StoryObj; + +export const ZeroPercent: Story = { + args: { + progress: 0, + }, +}; + +export const HalfComplete: Story = { + args: { + progress: 50, + description: 'Processing...', + }, +}; + +export const NearComplete: Story = { + args: { + progress: 95, + description: 'Almost done', + elapsed: '1m 30s', + }, +}; + +export const Complete: Story = { + args: { + progress: 100, + description: 'Done', + elapsed: '2m 15s', + }, +}; diff --git a/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx new file mode 100644 index 00000000..70c72247 --- /dev/null +++ b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx @@ -0,0 +1,30 @@ +import { Group, Progress, Stack, Text } from '@mantine/core'; + +export interface ProgressDisplayProps { + progress: number; + description?: string; + elapsed?: string; +} + +export function ProgressDisplay({ progress, description, elapsed }: ProgressDisplayProps) { + return ( + + + {description && ( + + {description} + + )} + + {progress}% + + + + {elapsed && ( + + {elapsed} + + )} + + ); +} diff --git a/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.stories.tsx b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.stories.tsx new file mode 100644 index 00000000..3c3ac7c9 --- /dev/null +++ b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { StatusIndicator } from './StatusIndicator'; + +const meta: Meta = { + title: 'Atoms/StatusIndicator', + component: StatusIndicator, +}; + +export default meta; +type Story = StoryObj; + +export const Connected: Story = { + args: { + status: 'connected', + latencyMs: 23, + }, +}; + +export const Connecting: Story = { + args: { + status: 'connecting', + }, +}; + +export const Disconnected: Story = { + args: { + status: 'disconnected', + }, +}; + +export const Failed: Story = { + args: { + status: 'failed', + }, +}; + +export const FailedWithRetries: Story = { + args: { + status: 'failed', + retryCount: 3, + }, +}; diff --git a/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx new file mode 100644 index 00000000..fc572c1a --- /dev/null +++ b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx @@ -0,0 +1,49 @@ +import { Flex, Group, Text } from '@mantine/core'; + +export interface StatusIndicatorProps { + status: 'connected' | 'connecting' | 'disconnected' | 'failed'; + latencyMs?: number; + retryCount?: number; +} + +const statusColorVar: Record = { + connected: 'var(--inspector-status-connected)', + connecting: 'var(--inspector-status-connecting)', + disconnected: 'var(--inspector-status-disconnected)', + failed: 'var(--inspector-status-failed)', +}; + +function getLabel( + status: StatusIndicatorProps['status'], + latencyMs?: number, + retryCount?: number, +): string { + switch (status) { + case 'connected': + return latencyMs != null ? `Connected (${latencyMs}ms)` : 'Connected'; + case 'connecting': + return 'Connecting...'; + case 'disconnected': + return 'Disconnected'; + case 'failed': + return retryCount != null ? `Failed (${retryCount})` : 'Failed'; + } +} + +const Dot = Flex.withProps({ + w: 10, + h: 10, + style: { borderRadius: '50%' }, +}); + +export function StatusIndicator({ status, latencyMs, retryCount }: StatusIndicatorProps) { + return ( + + + {getLabel(status, latencyMs, retryCount)} + + ); +} diff --git a/clients/web/src/components/atoms/TransportBadge/TransportBadge.stories.tsx b/clients/web/src/components/atoms/TransportBadge/TransportBadge.stories.tsx new file mode 100644 index 00000000..26682ec6 --- /dev/null +++ b/clients/web/src/components/atoms/TransportBadge/TransportBadge.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { TransportBadge } from './TransportBadge' + +const meta: Meta = { + title: 'Atoms/TransportBadge', + component: TransportBadge, +} + +export default meta +type Story = StoryObj + +export const Stdio: Story = { + args: { + transport: 'stdio', + }, +} + +export const Http: Story = { + args: { + transport: 'http', + }, +} diff --git a/clients/web/src/components/atoms/TransportBadge/TransportBadge.tsx b/clients/web/src/components/atoms/TransportBadge/TransportBadge.tsx new file mode 100644 index 00000000..dd206729 --- /dev/null +++ b/clients/web/src/components/atoms/TransportBadge/TransportBadge.tsx @@ -0,0 +1,13 @@ +import { Badge } from '@mantine/core' + +export interface TransportBadgeProps { + transport: 'stdio' | 'http' +} + +export function TransportBadge({ transport }: TransportBadgeProps) { + return ( + + {transport.toUpperCase()} + + ) +} diff --git a/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.stories.tsx b/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.stories.tsx new file mode 100644 index 00000000..b008d2c4 --- /dev/null +++ b/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { AddServerMenu } from './AddServerMenu'; + +const meta: Meta = { + title: 'Molecules/AddServerMenu', + component: AddServerMenu, + args: { + onAddManually: fn(), + onImportConfig: fn(), + onImportServerJson: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.tsx b/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.tsx new file mode 100644 index 00000000..cd30afe1 --- /dev/null +++ b/clients/web/src/components/molecules/AddServerMenu/AddServerMenu.tsx @@ -0,0 +1,32 @@ +import { Button, Menu } from '@mantine/core'; + +export interface AddServerMenuProps { + onAddManually: () => void; + onImportConfig: () => void; + onImportServerJson: () => void; +} + +export function AddServerMenu({ + onAddManually, + onImportConfig, + onImportServerJson, +}: AddServerMenuProps) { + return ( + + + + + + + + Add manually + + + Import config + + + Import server.json + + + + ); +} diff --git a/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.stories.tsx b/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.stories.tsx new file mode 100644 index 00000000..e89c1c2c --- /dev/null +++ b/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' +import { ElicitationFormPanel } from './ElicitationFormPanel' + +const meta: Meta = { + title: 'Molecules/ElicitationFormPanel', + component: ElicitationFormPanel, + args: { + onChange: fn(), + onSubmit: fn(), + onCancel: fn(), + serverName: 'postgres-server', + message: 'Please provide database connection details.', + values: {}, + }, +} + +export default meta +type Story = StoryObj + +export const SimpleForm: Story = { + args: { + schema: { + type: 'object', + properties: { + host: { type: 'string', title: 'Host' }, + port: { type: 'integer', title: 'Port' }, + database: { type: 'string', title: 'Database' }, + }, + required: ['host', 'port'], + }, + }, +} + +export const WithEnums: Story = { + args: { + schema: { + type: 'object', + properties: { + sslMode: { + type: 'string', + title: 'SSL Mode', + oneOf: [ + { const: 'disable', title: 'Disable' }, + { const: 'require', title: 'Require' }, + { const: 'verify-full', title: 'Verify Full' }, + ], + }, + }, + }, + }, +} + +export const AllRequired: Story = { + args: { + schema: { + type: 'object', + properties: { + host: { type: 'string', title: 'Host' }, + port: { type: 'integer', title: 'Port' }, + database: { type: 'string', title: 'Database' }, + }, + required: ['host', 'port', 'database'], + }, + }, +} diff --git a/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.tsx b/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.tsx new file mode 100644 index 00000000..2c59d6ea --- /dev/null +++ b/clients/web/src/components/molecules/ElicitationFormPanel/ElicitationFormPanel.tsx @@ -0,0 +1,43 @@ +import { Alert, Button, Divider, Group, Stack, Text } from '@mantine/core' +import type { JsonSchema } from '../SchemaForm/SchemaForm' +import { SchemaForm } from '../SchemaForm/SchemaForm' + +export interface ElicitationFormPanelProps { + message: string + schema: JsonSchema + values: Record + serverName: string + onChange: (values: Record) => void + onSubmit: () => void + onCancel: () => void +} + +export function ElicitationFormPanel({ + message, + schema, + values, + serverName, + onChange, + onSubmit, + onCancel, +}: ElicitationFormPanelProps) { + return ( + + + “{message}” + + + + + Only provide information you trust this server with. The server “{serverName}” is + requesting this data. + + + + + + + ) +} diff --git a/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.stories.tsx b/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.stories.tsx new file mode 100644 index 00000000..609b5132 --- /dev/null +++ b/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' +import { ElicitationUrlPanel } from './ElicitationUrlPanel' + +const meta: Meta = { + title: 'Molecules/ElicitationUrlPanel', + component: ElicitationUrlPanel, + args: { + onCopyUrl: fn(), + onOpenInBrowser: fn(), + onCancel: fn(), + message: 'Please authenticate with the external service.', + elicitationId: 'elicit-abc-123', + }, +} + +export default meta +type Story = StoryObj + +export const Waiting: Story = { + args: { + url: 'https://auth.example.com/oauth/authorize?client_id=mcp-inspector&redirect_uri=http://localhost:3000/callback', + isWaiting: true, + }, +} + +export const WithLongUrl: Story = { + args: { + url: 'https://auth.example.com/oauth/authorize?client_id=mcp-inspector&redirect_uri=http://localhost:3000/callback&scope=read+write+admin&state=xyzzy-12345&nonce=abcdef-67890&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256', + isWaiting: true, + }, +} + +export const Completed: Story = { + args: { + url: 'https://auth.example.com/oauth/authorize?client_id=mcp-inspector', + isWaiting: false, + }, +} diff --git a/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.tsx b/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.tsx new file mode 100644 index 00000000..47d794bd --- /dev/null +++ b/clients/web/src/components/molecules/ElicitationUrlPanel/ElicitationUrlPanel.tsx @@ -0,0 +1,60 @@ +import { Alert, Button, Code, Divider, Group, Loader, Stack, Text } from '@mantine/core' + +export interface ElicitationUrlPanelProps { + message: string + url: string + elicitationId: string + isWaiting: boolean + onCopyUrl: () => void + onOpenInBrowser: () => void + onCancel: () => void +} + +export function ElicitationUrlPanel({ + message, + url, + elicitationId, + isWaiting, + onCopyUrl, + onOpenInBrowser, + onCancel, +}: ElicitationUrlPanelProps) { + return ( + + + {message} + + + The server is requesting you visit: + {url} + + + + + + {isWaiting && ( + + + + Waiting for completion... + + + )} + + Elicitation ID: {elicitationId} + + + This will open an external URL. Verify the domain before proceeding. + + + + + + ) +} diff --git a/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx b/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx new file mode 100644 index 00000000..fde3a98e --- /dev/null +++ b/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.stories.tsx @@ -0,0 +1,118 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' +import { ExperimentalFeaturesPanel } from './ExperimentalFeaturesPanel' + +const meta: Meta = { + title: 'Molecules/ExperimentalFeaturesPanel', + component: ExperimentalFeaturesPanel, + args: { + onToggleClientCapability: fn(), + onRequestChange: fn(), + onSendRequest: fn(), + onAddHeader: fn(), + onRemoveHeader: fn(), + onHeaderChange: fn(), + onCopyResponse: fn(), + clientCapabilities: [ + { name: 'experimental/customSampling', enabled: false }, + { name: 'experimental/batchRequests', enabled: true }, + ], + customHeaders: [], + requestHistory: [], + requestJson: JSON.stringify( + { + jsonrpc: '2.0', + id: 1, + method: 'experimental/myMethod', + params: {}, + }, + null, + 2 + ), + }, +} + +export default meta +type Story = StoryObj + +export const WithServerCaps: Story = { + args: { + serverCapabilities: [ + { + name: 'experimental/streaming', + description: 'Supports streaming responses for long-running operations', + methods: ['experimental/stream.start', 'experimental/stream.cancel'], + }, + { + name: 'experimental/caching', + description: 'Server-side response caching with TTL support', + methods: ['experimental/cache.get', 'experimental/cache.set', 'experimental/cache.clear'], + }, + ], + }, +} + +export const NoServerCaps: Story = { + args: { + serverCapabilities: [], + }, +} + +export const WithResponse: Story = { + args: { + serverCapabilities: [ + { + name: 'experimental/echo', + description: 'Echoes back the request for testing', + methods: ['experimental/echo'], + }, + ], + responseJson: JSON.stringify( + { + jsonrpc: '2.0', + id: 1, + result: { + echo: 'Hello from experimental endpoint', + timestamp: '2026-03-17T10:30:00Z', + }, + }, + null, + 2 + ), + customHeaders: [ + { key: 'X-Custom-Auth', value: 'Bearer token123' }, + { key: 'X-Request-Id', value: 'req-abc-456' }, + ], + }, +} + +export const WithHistory: Story = { + args: { + serverCapabilities: [ + { + name: 'experimental/metrics', + description: 'Exposes server metrics', + }, + ], + requestHistory: [ + { + timestamp: '2026-03-17 10:30:15', + method: 'experimental/metrics.get', + status: 'success', + durationMs: 42, + }, + { + timestamp: '2026-03-17 10:29:50', + method: 'experimental/echo', + status: 'success', + durationMs: 15, + }, + { + timestamp: '2026-03-17 10:28:30', + method: 'experimental/unknown', + status: 'error', + durationMs: 120, + }, + ], + }, +} diff --git a/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx b/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx new file mode 100644 index 00000000..e27e0596 --- /dev/null +++ b/clients/web/src/components/molecules/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx @@ -0,0 +1,207 @@ +import { + ActionIcon, + Alert, + Button, + Card, + Checkbox, + Divider, + Group, + Stack, + Table, + Text, + TextInput, + Textarea, + Title, +} from '@mantine/core' + +export interface ExperimentalCapability { + name: string + description?: string + methods?: string[] +} + +export interface ClientExperimentalCapability { + name: string + enabled: boolean +} + +export interface KeyValuePair { + key: string + value: string +} + +export interface RequestHistoryItem { + timestamp: string + method: string + status: string + durationMs: number +} + +export interface ExperimentalFeaturesPanelProps { + serverCapabilities: ExperimentalCapability[] + clientCapabilities: ClientExperimentalCapability[] + requestJson: string + responseJson?: string + customHeaders: KeyValuePair[] + requestHistory: RequestHistoryItem[] + onToggleClientCapability: (name: string, enabled: boolean) => void + onRequestChange: (json: string) => void + onSendRequest: () => void + onAddHeader: () => void + onRemoveHeader: (index: number) => void + onHeaderChange: (index: number, key: string, value: string) => void + onCopyResponse: () => void +} + +export function ExperimentalFeaturesPanel({ + serverCapabilities, + clientCapabilities, + requestJson, + responseJson, + customHeaders, + requestHistory, + onToggleClientCapability, + onRequestChange, + onSendRequest, + onAddHeader, + onRemoveHeader, + onHeaderChange, + onCopyResponse, +}: ExperimentalFeaturesPanelProps) { + return ( + + + These features are non-standard and may change or be removed. + + + Server Experimental Capabilities: + + {serverCapabilities.length === 0 ? ( + No experimental capabilities + ) : ( + serverCapabilities.map((cap) => ( + + + {cap.name} + {cap.description && ( + + {cap.description} + + )} + {cap.methods && cap.methods.length > 0 && ( + + Methods: {cap.methods.join(', ')} + + )} + + + + + + )) + )} + + + + Client Experimental Capabilities: + + {clientCapabilities.map((clientCap) => ( + onToggleClientCapability(clientCap.name, e.currentTarget.checked)} + /> + ))} + + + + Advanced JSON-RPC Tester + + + Send raw JSON-RPC requests to test ANY method + + + {customHeaders.map((header, index) => ( + + onHeaderChange(index, e.currentTarget.value, header.value)} + /> + onHeaderChange(index, header.key, e.currentTarget.value)} + /> + onRemoveHeader(index)}> + ✕ + + + ))} + + + + + +