├── .env.example ├── .github └── workflows │ ├── build-test-lint.yml │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── 2025-04-21T210500_NEXT_PROMPT.md ├── Cargo.lock ├── Cargo.toml ├── IMPLEMENTATION_PLAN.md ├── README.md ├── apps ├── web │ ├── .env.example │ ├── components.json │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── _redirects │ │ └── logos │ │ │ ├── app_icon_240x240.png │ │ │ ├── favicon_32x32.png │ │ │ ├── pnpm.svg │ │ │ ├── stripe.svg │ │ │ ├── supabase-light.svg │ │ │ ├── supabase.svg │ │ │ └── vite.svg │ ├── setupTests.ts │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── __forceRebuild__.ts │ │ ├── components │ │ │ ├── ai │ │ │ │ ├── AiChatbox.test.tsx │ │ │ │ ├── AiChatbox.tsx │ │ │ │ ├── ChatContextSelector.test.tsx │ │ │ │ ├── ChatContextSelector.tsx │ │ │ │ ├── ChatHistoryList.test.tsx │ │ │ │ ├── ChatHistoryList.tsx │ │ │ │ ├── ChatItem.test.tsx │ │ │ │ ├── ChatItem.tsx │ │ │ │ ├── ChatMessageBubble.test.tsx │ │ │ │ ├── ChatMessageBubble.tsx │ │ │ │ ├── ModelSelector.test.tsx │ │ │ │ ├── ModelSelector.tsx │ │ │ │ ├── PromptSelector.test.tsx │ │ │ │ ├── PromptSelector.tsx │ │ │ │ └── ai.integration.test.tsx │ │ │ ├── auth │ │ │ │ ├── AuthenticatedGate.test.tsx │ │ │ │ ├── AuthenticatedGate.tsx │ │ │ │ ├── LoginForm.test.tsx │ │ │ │ ├── LoginForm.tsx │ │ │ │ ├── ProtectedRoute.test.tsx │ │ │ │ ├── ProtectedRoute.tsx │ │ │ │ ├── RegisterForm.test.tsx │ │ │ │ ├── RegisterForm.tsx │ │ │ │ └── auth.integration.test.tsx │ │ │ ├── common │ │ │ │ ├── AttributionDisplay.test.tsx │ │ │ │ ├── AttributionDisplay.tsx │ │ │ │ ├── DropZone.test.tsx │ │ │ │ ├── DropZone.tsx │ │ │ │ ├── ErrorBoundary.test.tsx │ │ │ │ ├── ErrorBoundary.tsx │ │ │ │ ├── FileDataDisplay.test.tsx │ │ │ │ ├── FileDataDisplay.tsx │ │ │ │ ├── MarkdownRenderer.test.tsx │ │ │ │ ├── MarkdownRenderer.tsx │ │ │ │ ├── PaginationComponent.test.tsx │ │ │ │ ├── PaginationComponent.tsx │ │ │ │ ├── TextInputArea.test.tsx │ │ │ │ └── TextInputArea.tsx │ │ │ ├── debug │ │ │ │ ├── PlatformFeatureTester.test.tsx │ │ │ │ └── PlatformFeatureTester.tsx │ │ │ ├── demos │ │ │ │ └── WalletBackupDemo │ │ │ │ │ ├── ExportMnemonicButton.test.tsx │ │ │ │ │ ├── ExportMnemonicButton.tsx │ │ │ │ │ ├── GenerateMnemonicButton.test.tsx │ │ │ │ │ ├── GenerateMnemonicButton.tsx │ │ │ │ │ ├── ImportMnemonicButton.test.tsx │ │ │ │ │ ├── ImportMnemonicButton.tsx │ │ │ │ │ ├── StatusDisplay.test.tsx │ │ │ │ │ ├── StatusDisplay.tsx │ │ │ │ │ ├── WalletBackupDemoCard.test.tsx │ │ │ │ │ └── WalletBackupDemoCard.tsx │ │ │ ├── features │ │ │ │ ├── ConfigFileManager.test.tsx │ │ │ │ └── ConfigFileManager.tsx │ │ │ ├── integrations │ │ │ │ └── ChatwootIntegration.tsx │ │ │ ├── layout │ │ │ │ ├── Footer.test.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.test.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ └── Login.test.tsx │ │ │ ├── marketing │ │ │ │ └── Badges.tsx │ │ │ ├── notifications │ │ │ │ ├── NotificationCard.tsx │ │ │ │ └── Notifications.tsx │ │ │ ├── organizations │ │ │ │ ├── AdminBadge.tsx │ │ │ │ ├── CreateOrganizationForm.test.tsx │ │ │ │ ├── CreateOrganizationForm.tsx │ │ │ │ ├── CreateOrganizationModal.test.tsx │ │ │ │ ├── CreateOrganizationModal.tsx │ │ │ │ ├── DeleteOrganizationDialog.test.tsx │ │ │ │ ├── DeleteOrganizationDialog.tsx │ │ │ │ ├── InviteMemberCard.test.tsx │ │ │ │ ├── InviteMemberCard.tsx │ │ │ │ ├── MemberListCard.test.tsx │ │ │ │ ├── MemberListCard.tsx │ │ │ │ ├── OrganizationChatSettings.test.tsx │ │ │ │ ├── OrganizationChatSettings.tsx │ │ │ │ ├── OrganizationDetailsCard.test.tsx │ │ │ │ ├── OrganizationDetailsCard.tsx │ │ │ │ ├── OrganizationListCard.test.tsx │ │ │ │ ├── OrganizationListCard.tsx │ │ │ │ ├── OrganizationSettingsCard.test.tsx │ │ │ │ ├── OrganizationSettingsCard.tsx │ │ │ │ ├── OrganizationSwitcher.test.tsx │ │ │ │ ├── OrganizationSwitcher.tsx │ │ │ │ ├── PendingActionsCard.test.tsx │ │ │ │ └── PendingActionsCard.tsx │ │ │ ├── profile │ │ │ │ ├── ProfileEditor.test.tsx │ │ │ │ ├── ProfileEditor.tsx │ │ │ │ ├── ProfilePrivacySettingsCard.tsx │ │ │ │ ├── ProfilePrivacySettingsCard.unit.test.tsx │ │ │ │ └── profile.integration.test.tsx │ │ │ ├── routes │ │ │ │ ├── RootRoute.test.tsx │ │ │ │ ├── RootRoute.tsx │ │ │ │ └── TauriOnlyWrapper.tsx │ │ │ ├── subscription │ │ │ │ ├── CurrentSubscriptionCard.test.tsx │ │ │ │ ├── CurrentSubscriptionCard.tsx │ │ │ │ ├── PlanCard.test.tsx │ │ │ │ ├── PlanCard.tsx │ │ │ │ └── Subscription.integration.test.tsx │ │ │ └── ui │ │ │ │ ├── SimpleDropdown.tsx │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── carousel.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── menubar.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sidebar.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ ├── config │ │ │ └── themes.ts │ │ ├── context │ │ │ └── theme.context.tsx │ │ ├── hooks │ │ │ ├── use-mobile.ts │ │ │ ├── useAuthSession.test.ts │ │ │ ├── useCurrentUser.ts │ │ │ ├── useSubscription.ts │ │ │ ├── useTheme.test.tsx │ │ │ └── useTheme.ts │ │ ├── index.css │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── mocks │ │ │ ├── aiStore.mock.ts │ │ │ ├── authStore.mock.ts │ │ │ └── organizationStore.mock.ts │ │ ├── pages │ │ │ ├── AcceptInvitePage.test.tsx │ │ │ ├── AcceptInvitePage.tsx │ │ │ ├── AiChat.test.tsx │ │ │ ├── AiChat.tsx │ │ │ ├── Dashboard.test.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── Home.test.tsx │ │ │ ├── Home.tsx │ │ │ ├── Login.tsx │ │ │ ├── Notifications.test.tsx │ │ │ ├── Notifications.tsx │ │ │ ├── OrganizationFocusedViewPage.test.tsx │ │ │ ├── OrganizationFocusedViewPage.tsx │ │ │ ├── OrganizationHubPage.test.tsx │ │ │ ├── OrganizationHubPage.tsx │ │ │ ├── Profile.test.tsx │ │ │ ├── Profile.tsx │ │ │ ├── Register.test.tsx │ │ │ ├── Register.tsx │ │ │ ├── Subscription.test.tsx │ │ │ ├── Subscription.tsx │ │ │ ├── SubscriptionSuccess.test.tsx │ │ │ └── SubscriptionSuccess.tsx │ │ ├── routes │ │ │ └── routes.tsx │ │ ├── tests │ │ │ └── setup.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts └── windows │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src-tauri │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── crates │ │ ├── core-crypto │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ └── storage-interface │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ ├── capabilities.rs │ │ ├── crypto_commands.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── wallet_commands.rs │ └── tauri.conf.json │ ├── tsconfig.json │ └── vite.config.ts ├── deno.lock ├── docs ├── architecture │ └── tauri_bridge.md ├── dev_plan.md ├── development │ └── PlatformCapabilities.md ├── imp_plan.md ├── implementations │ ├── Complete-Closed-Deprecated-Discarded │ │ ├── 20250416 Anon Auth Flow.md │ │ ├── 20250418 Analytics Clients.md │ │ ├── 20250419 Email Marketing.md │ │ ├── 20250421 AI Providers Integration.md │ │ ├── 20250421-markdown-rendering.md │ │ ├── 20250422_Multitenant.md │ │ ├── 20250422_Notifications.md │ │ ├── 20250422_Platform Capabilities.md │ │ ├── 20250423112244_Supabase_Auth_Fix.md │ │ ├── 20250502_Merged_Platform_Capabilities.md │ │ ├── 20250502_Org_Refinement.md │ │ ├── 20250502_desktop_ui.md │ │ ├── 20250503_WalletBackupDemo.md │ │ └── Consolidate Zustand.md │ ├── Current │ │ ├── Checklists │ │ │ ├── Complete │ │ │ │ ├── 0-Project-Setup-and-Planning.md │ │ │ │ ├── 1-Core-Backend-and-Data-Model.md │ │ │ │ ├── 2-State-Management-Integration.md │ │ │ │ └── 3-UI-Component-Implementation.md │ │ │ ├── Current │ │ │ │ ├── 4-Token-Tracking-Audit.md │ │ │ │ └── desktop-crypto │ │ │ │ │ ├── 20250504_WalletBackend.md │ │ │ │ │ ├── NextSteps_After_WalletBackend.md │ │ │ │ │ ├── documentation │ │ │ │ │ ├── DEV_PLAN.md │ │ │ │ │ ├── IMPLEMENTATION_PLAN.md │ │ │ │ │ ├── STREAMING_SCALABILITY_AND_ORACLE_DESIGN.md │ │ │ │ │ ├── STRUCTURE.md │ │ │ │ │ ├── TESTING_PLAN.md │ │ │ │ │ ├── cryptography.md │ │ │ │ │ ├── key_management.md │ │ │ │ │ └── solidity_contract_draft.md │ │ │ │ │ └── genesis │ │ │ │ │ ├── 0. always │ │ │ │ │ └── always.md │ │ │ │ │ ├── 1. thesis │ │ │ │ │ ├── seedphrase.md │ │ │ │ │ ├── thesis.claude.md │ │ │ │ │ ├── thesis.gemini.md │ │ │ │ │ └── thesis.openai.md │ │ │ │ │ ├── 2. antithesis │ │ │ │ │ ├── antithesis.claude.md │ │ │ │ │ ├── antithesis.gemini.md │ │ │ │ │ ├── antithesis.openai.md │ │ │ │ │ └── seedphrase │ │ │ │ │ ├── 3. synthesis │ │ │ │ │ ├── seedphrase.md │ │ │ │ │ ├── synthesis.claude.md │ │ │ │ │ ├── synthesis.gemini.md │ │ │ │ │ └── synthesis.openai.md │ │ │ │ │ └── 4. implementation │ │ │ │ │ └── 20250426_crypto_core.md │ │ │ └── Pending │ │ │ │ ├── 4.5-Organization-Finder.md │ │ │ │ ├── 4.6-Multi-User-Chat.md │ │ │ │ ├── 5-Bug-Fixes-and-Optimizations.md │ │ │ │ ├── 6-Stretch-Goals.md │ │ │ │ ├── 7-Testing-and-Refinement.md │ │ │ │ ├── 8-Documentation-and-Deployment.md │ │ │ │ └── 9-Post-Implementation.md │ │ ├── Documentation │ │ │ ├── 0-2-1-FilePaths.md │ │ │ └── 0-2-2-ArchitectureAndTypes.md │ │ ├── fix_pending_invites_display.md │ │ └── process │ │ │ ├── 1. hypothesis │ │ │ ├── ai-chat-enhancements-hypothesis-claude.md │ │ │ ├── ai-chat-enhancements-hypothesis-gemini.md │ │ │ ├── ai-chat-enhancements-hypothesis-openai.md │ │ │ ├── ai-chat-enhancements-hypothesis-seed.md │ │ │ └── ai-chat-enhancements-hypothesis-user-response.md │ │ │ ├── 2. antithesis │ │ │ ├── ai-chat-enhancements-antithesis-claude.md │ │ │ ├── ai-chat-enhancements-antithesis-gemini.md │ │ │ ├── ai-chat-enhancements-antithesis-openai.md │ │ │ ├── ai-chat-enhancements-antithesis-seed.md │ │ │ └── ai-chat-enhancements-antithesis-user-response.md │ │ │ ├── 3. synthesis │ │ │ ├── ai-chat-enhancements-synthesis-claude.md │ │ │ ├── ai-chat-enhancements-synthesis-gemini.md │ │ │ ├── ai-chat-enhancements-synthesis-openai.md │ │ │ ├── ai-chat-enhancements-synthesis-seed.md │ │ │ └── ai-chat-enhancements-synthesis-user-response.md │ │ │ └── 4. parenthesis │ │ │ ├── ai-chat-enhancements-parenthesis-claude.md │ │ │ ├── ai-chat-enhancements-parenthesis-gemini.md │ │ │ ├── ai-chat-enhancements-parenthesis-openai.md │ │ │ └── ai-chat-enhancements-parenthesis-seed.md │ └── Pending │ │ ├── 20250422_Refactor_Types.md │ │ ├── 20250424174735_auth_req_cleanup.md │ │ ├── 20250430_FixAuthListenerHandling.md │ │ └── 20250506_Finish_Initial_Testing.md ├── struct.md └── tests.md ├── eslint.config.js ├── nx.json ├── package.json ├── packages ├── analytics │ ├── package.json │ ├── src │ │ ├── .keep │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── nullAdapter.test.ts │ │ ├── nullAdapter.ts │ │ ├── posthogAdapter.test.ts │ │ └── posthogAdapter.ts │ └── tsconfig.json ├── api │ ├── package.json │ ├── src │ │ ├── ApiContext.tsx │ │ ├── ai.api.test.ts │ │ ├── ai.api.ts │ │ ├── apiClient.test.ts │ │ ├── apiClient.ts │ │ ├── fetch.test.ts │ │ ├── index.ts │ │ ├── mocks │ │ │ ├── ai.api.mock.ts │ │ │ ├── apiClient.mock.ts │ │ │ ├── chatHistory.mock.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── notifications.api.mock.ts │ │ │ ├── organizations.api.mock.ts │ │ │ ├── organizations.mock.ts │ │ │ ├── stripe.mock.ts │ │ │ └── supabase.mock.ts │ │ ├── notifications.api.test.ts │ │ ├── notifications.api.ts │ │ ├── organizations.api.test.ts │ │ ├── organizations.api.ts │ │ ├── setupTests.ts │ │ ├── stripe.api.test.ts │ │ ├── stripe.api.ts │ │ ├── users.api.test.ts │ │ ├── users.api.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── platform │ ├── package.json │ ├── src │ │ ├── Context.test.tsx │ │ ├── Context.tsx │ │ ├── android.test.ts │ │ ├── android.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── ios.test.ts │ │ ├── ios.ts │ │ ├── linux.test.ts │ │ ├── linux.ts │ │ ├── mac.test.ts │ │ ├── mac.ts │ │ ├── setupTests.ts │ │ ├── tauri.test.ts │ │ ├── tauri.ts │ │ ├── web.test.ts │ │ └── web.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── store │ ├── package.json │ ├── src │ │ ├── aiStore.config.test.ts │ │ ├── aiStore.context.test.ts │ │ ├── aiStore.deleteChat.test.ts │ │ ├── aiStore.details.test.ts │ │ ├── aiStore.history.test.ts │ │ ├── aiStore.hydration.test.ts │ │ ├── aiStore.misc.test.ts │ │ ├── aiStore.replay.test.ts │ │ ├── aiStore.rewind.test.ts │ │ ├── aiStore.selectors.test.ts │ │ ├── aiStore.selectors.ts │ │ ├── aiStore.sendMessage.test.ts │ │ ├── aiStore.startNewChat.test.ts │ │ ├── aiStore.ts │ │ ├── authStore.base.test.ts │ │ ├── authStore.listener.test.ts │ │ ├── authStore.login.test.ts │ │ ├── authStore.logout.test.ts │ │ ├── authStore.profile.test.ts │ │ ├── authStore.register.test.ts │ │ ├── authStore.ts │ │ ├── index.ts │ │ ├── notificationStore.test.ts │ │ ├── notificationStore.ts │ │ ├── organizationStore.selectors.test.ts │ │ ├── organizationStore.selectors.ts │ │ ├── organizationStore.settings.test.ts │ │ ├── organizationStore.test.ts │ │ ├── organizationStore.ts │ │ ├── setupTests.ts │ │ ├── subscriptionStore.selectors.test.ts │ │ ├── subscriptionStore.selectors.ts │ │ ├── subscriptionStore.test.ts │ │ └── subscriptionStore.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── types │ ├── package.json │ ├── src │ │ ├── ai.types.ts │ │ ├── analytics.types.ts │ │ ├── api.types.ts │ │ ├── auth.types.ts │ │ ├── email.types.ts │ │ ├── index.ts │ │ ├── logger.types.ts │ │ ├── navigation.types.ts │ │ ├── notification.types.ts │ │ ├── organizations.types.ts │ │ ├── platform.types.ts │ │ ├── route.types.ts │ │ ├── subscription.types.ts │ │ ├── supabase.types.ts │ │ ├── theme.types.ts │ │ └── vite-env.d.ts │ └── tsconfig.json └── utils │ ├── package.json │ ├── src │ ├── ai-parsers.ts │ ├── index.ts │ ├── logger.test.ts │ ├── logger.ts │ ├── platform.ts │ └── stringUtils.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── build.js └── update-imports.js ├── supabase ├── .gitignore ├── config.toml ├── deno.json ├── deno.lock ├── functions │ ├── .gitignore │ ├── README.md │ ├── _shared │ │ ├── ai_service │ │ │ ├── anthropic_adapter.test.ts │ │ │ ├── anthropic_adapter.ts │ │ │ ├── factory.test.ts │ │ │ ├── factory.ts │ │ │ ├── google_adapter.test.ts │ │ │ ├── google_adapter.ts │ │ │ ├── openai_adapter.test.ts │ │ │ └── openai_adapter.ts │ │ ├── auth.test.ts │ │ ├── auth.ts │ │ ├── cors-headers.test.ts │ │ ├── cors-headers.ts │ │ ├── email_service │ │ │ ├── dummy_service.ts │ │ │ ├── factory.test.ts │ │ │ ├── factory.ts │ │ │ ├── kit_service.test.ts │ │ │ ├── kit_service.ts │ │ │ ├── no_op_service.test.ts │ │ │ └── no_op_service.ts │ │ ├── logger.ts │ │ ├── stripe-client.test.ts │ │ ├── stripe-client.ts │ │ ├── supabase.mock.ts │ │ └── types.ts │ ├── ai-providers │ │ ├── index.test.ts │ │ └── index.ts │ ├── api-subscriptions │ │ ├── api-subscriptions.integration.test.ts │ │ ├── handlers │ │ │ ├── billing-portal.test.ts │ │ │ ├── billing-portal.ts │ │ │ ├── checkout.test.ts │ │ │ ├── checkout.ts │ │ │ ├── current.test.ts │ │ │ ├── current.ts │ │ │ ├── plans.test.ts │ │ │ ├── plans.ts │ │ │ ├── subscription.test.ts │ │ │ ├── subscription.ts │ │ │ ├── usage.test.ts │ │ │ └── usage.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── chat-details │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── test │ │ │ └── chat-details.integration.test.ts │ ├── chat-history │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── test │ │ │ └── chat-history.integration.test.ts │ ├── chat │ │ ├── _server.ts │ │ ├── dependencies.ts │ │ ├── index.rewind.test.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── test │ │ │ └── chat.integration.test.ts │ ├── deno.jsonc │ ├── deno.lock │ ├── login │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── login.integration.test.ts │ ├── logout │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── logout.integration.test.ts │ ├── me │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── me.integration.test.ts │ ├── notifications │ │ ├── index.test.ts │ │ └── index.ts │ ├── on-user-created │ │ ├── index.test.ts │ │ └── index.ts │ ├── organizations │ │ ├── create.test.ts │ │ ├── create.ts │ │ ├── details.test.ts │ │ ├── details.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── invites.test.ts │ │ ├── invites.ts │ │ ├── list.test.ts │ │ ├── list.ts │ │ ├── members.test.ts │ │ ├── members.ts │ │ ├── requests.test.ts │ │ └── requests.ts │ ├── package-lock.json │ ├── package.json │ ├── ping │ │ ├── index.ts │ │ └── ping.integration.test.ts │ ├── profile │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── profile.integration.test.ts │ ├── register │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── register.integration.test.ts │ ├── reset-password │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── reset-password.integration.test.ts │ ├── session │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── session.integration.test.ts │ ├── stripe-webhook │ │ ├── handlers │ │ │ ├── checkout-session.test.ts │ │ │ ├── checkout-session.ts │ │ │ ├── invoice.test.ts │ │ │ ├── invoice.ts │ │ │ ├── price.test.ts │ │ │ ├── price.ts │ │ │ ├── product.test.ts │ │ │ ├── product.ts │ │ │ ├── subscription.test.ts │ │ │ └── subscription.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── services │ │ │ ├── price_webhook_service.ts │ │ │ └── product_webhook_service.ts │ │ └── stripe-webhook.integration.test.ts │ ├── sync-ai-models │ │ ├── anthropic_sync.test.ts │ │ ├── anthropic_sync.ts │ │ ├── google_sync.test.ts │ │ ├── google_sync.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── openai_sync.test.ts │ │ └── openai_sync.ts │ ├── sync-stripe-plans │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── services │ │ │ └── sync_plans_service.ts │ ├── system-prompts │ │ └── index.ts │ ├── tools │ │ └── sync-envs.js │ ├── tsconfig.json │ └── types_db.ts ├── import_map.json ├── integration_tests │ ├── me.integration.test.ts │ └── rls │ │ ├── delete_chat_function.test.ts │ │ ├── rls_chat_messages.test.ts │ │ ├── rls_chats.test.ts │ │ └── rls_test_helpers.ts ├── migrations │ └── 20250512205139_remote_schema.sql ├── package-lock.json ├── package.json ├── project.json ├── scripts │ ├── apply_email_sync_trigger.ts │ └── sync-supabase-shared-types.mjs ├── seed.sql ├── supabase.test.output.md └── tests │ └── database │ ├── chat_restrictions_policy_test.sql │ ├── chat_seed_file.sql │ ├── chat_seed_markdown.sql │ └── org_rls_policy_test_test.sql ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.node.json └── vitest.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Supabase Keys 2 | VITE_SUPABASE_ANON_KEY= 3 | VITE_SUPABASE_URL=https://[YOUR SUPABASE PROJECT ID].supabase.co/ 4 | VITE_SUPABASE_SERVICE_ROLE_KEY= 5 | SUPABASE_DATABASE_PASSWORD= ## You'll need this for the email sync trigger script 6 | 7 | ## AI Keys 8 | OPENAI_API_KEY= 9 | ANTHROPIC_API_KEY= 10 | GOOGLE_API_KEY= 11 | PERPLEXITY_API_KEY= 12 | DEEPSEEK_API_KEY= 13 | GROQ_API_KEY= 14 | 15 | ## Stripe live 16 | STRIPE_SECRET_LIVE_KEY= 17 | STRIPE_LIVE_WEBHOOK_SECRET= 18 | 19 | ## Stripe test 20 | VITE_STRIPE_TEST_MODE=true ## Set to "false" when you want Stripe to actually run transactions 21 | STRIPE_SECRET_TEST_KEY= 22 | STRIPE_TEST_WEBHOOK_SECRET= 23 | 24 | ## Analytics 25 | VITE_GA_MEASUREMENT_ID= ## your Google Analytics ID (e.g., G-XXXXXXXXXX) 26 | 27 | ## Customer Support 28 | VITE_CHATWOOT_BASE_URL= ## e.g., https://app.chatwoot.com 29 | VITE_CHATWOOT_WEBSITE_TOKEN= ## Your Chatwoot Website Inbox Token 30 | 31 | ## Email Marketing 32 | EMAIL_MARKETING_PROVIDER="none" # Options: "kit", "none" 33 | EMAIL_MARKETING_BASE_URL="https://api.convertkit.com" # Base URL for Kit API (v3) 34 | EMAIL_MARKETING_API_KEY="" # Required for Kit 35 | EMAIL_MARKETING_TAG_ID="" # Required for Kit (Tag to add users to) 36 | EMAIL_MARKETING_CUSTOM_USER_ID_FIELD="" # Required for Kit (e.g., "cf_supabase_user_id") 37 | EMAIL_MARKETING_CUSTOM_CREATED_AT_FIELD="" # Required for Kit (e.g., "cf_supabase_created_at") 38 | 39 | # Analytics (Optional) 40 | # Set VITE_ANALYTICS_PROVIDER to 'posthog' (or 'mixpanel' in future) to enable. 41 | # If provider is set, the corresponding API key is required. 42 | VITE_ANALYTICS_PROVIDER="none" # Options: "none", "posthog" 43 | VITE_POSTHOG_KEY="" # Required if provider is "posthog" 44 | VITE_POSTHOG_HOST="https://app.posthog.com" # Optional: Defaults to PostHog cloud -------------------------------------------------------------------------------- /.github/workflows/build-test-lint.yml: -------------------------------------------------------------------------------- 1 | name: Build, Lint, and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop # Add other main branches if applicable 8 | - 'feature/**' 9 | pull_request: 10 | branches: 11 | - main 12 | - develop # Add other main branches if applicable 13 | 14 | jobs: 15 | build_lint_test: 16 | name: Build, Lint & Test 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v4 25 | with: 26 | version: 9 # Specify your pnpm version if needed 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '20' # Specify your Node.js version 32 | cache: 'pnpm' 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Lint Packages Individually 38 | run: pnpm -r lint 39 | 40 | - name: Build 41 | # Adjust if your build command is different or needs filtering 42 | run: pnpm build 43 | 44 | - name: Test 45 | run: pnpm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # env files 27 | .env 28 | 29 | tsconfig.tsbuildinfo 30 | .nx/cache 31 | .nx/workspace-data 32 | vite.config.*.timestamp* 33 | vitest.config.*.timestamp* 34 | 35 | # Rust 36 | target/ 37 | 38 | 39 | # Rust build artifacts 40 | /target/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semiColons": false, 5 | "bracketSpacing": true, 6 | "proseWrap": "preserve", 7 | "semi": false, 8 | "singleQuote": true, 9 | "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], 10 | "importOrderSeparation": true, 11 | "importOrderSortSpecifiers": true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" # Explicitly set resolver version 3 | members = [ 4 | "apps/windows/src-tauri", # Path to the Tauri app crate 5 | "apps/windows/src-tauri/crates/core-crypto", # UPDATED Path to the new crypto crate location 6 | "apps/windows/src-tauri/crates/storage-interface" # ADDED storage-interface crate 7 | ] 8 | 9 | # Optional: Define shared dependencies or profiles 10 | [profile.release] 11 | lto = true 12 | codegen-units = 1 13 | panic = 'abort' -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | VITE_SUPABASE_ANON_KEY= 2 | VITE_SUPABASE_URL=https://[YOUR SUPABASE PROJECT ID].supabase.co/ 3 | VITE_APP_TITLE=YOUR APP NAME -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactRefresh from 'eslint-plugin-react-refresh'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /apps/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /apps/web/public/logos/app_icon_240x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/web/public/logos/app_icon_240x240.png -------------------------------------------------------------------------------- /apps/web/public/logos/favicon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/web/public/logos/favicon_32x32.png -------------------------------------------------------------------------------- /apps/web/public/logos/pnpm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/web/public/logos/stripe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/logos/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { cleanup } from '@testing-library/react'; 3 | import { afterEach, vi } from 'vitest'; 4 | import { setupMockServer } from './src/test-utils/mocks/api'; 5 | 6 | // Setup MSW 7 | setupMockServer(); 8 | 9 | // Cleanup after each test 10 | afterEach(() => { 11 | cleanup(); 12 | vi.clearAllMocks(); 13 | }); 14 | 15 | // Mock window.matchMedia 16 | Object.defineProperty(window, 'matchMedia', { 17 | writable: true, 18 | value: vi.fn().mockImplementation(query => ({ 19 | matches: false, 20 | media: query, 21 | onchange: null, 22 | addListener: vi.fn(), 23 | removeListener: vi.fn(), 24 | addEventListener: vi.fn(), 25 | removeEventListener: vi.fn(), 26 | dispatchEvent: vi.fn(), 27 | })), 28 | }); -------------------------------------------------------------------------------- /apps/web/src/__forceRebuild__.ts: -------------------------------------------------------------------------------- 1 | // Change this string when you're losing your mind 2 | export const forceRebuild = 'v1'; -------------------------------------------------------------------------------- /apps/web/src/components/auth/AuthenticatedGate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuthStore } from '@paynless/store'; 3 | 4 | // Placeholder for a loading component 5 | const DefaultLoadingFallback = () => ( 6 |
7 |
8 | ); 9 | 10 | interface AuthenticatedGateProps { 11 | children: React.ReactNode; 12 | loadingFallback?: React.ReactNode; 13 | unauthenticatedFallback?: React.ReactNode | null; 14 | } 15 | 16 | export function AuthenticatedGate({ 17 | children, 18 | loadingFallback = , 19 | unauthenticatedFallback = null, 20 | }: AuthenticatedGateProps) { 21 | 22 | const { user, isLoading } = useAuthStore(state => ({ 23 | user: state.user, 24 | isLoading: state.isLoading, 25 | })); 26 | 27 | if (isLoading) { 28 | // Still determining auth state 29 | return <>{loadingFallback}; 30 | } 31 | 32 | if (!user) { 33 | // Not authenticated, render null. Router handles showing public pages. 34 | return <>{unauthenticatedFallback}; 35 | } 36 | 37 | // Authenticated and auth check complete, render the protected children 38 | return <>{children}; 39 | } -------------------------------------------------------------------------------- /apps/web/src/components/auth/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | import { useAuthStore } from '@paynless/store'; 4 | import { UserRole } from '@paynless/types'; 5 | import { CreateOrganizationModal } from '../organizations/CreateOrganizationModal'; 6 | 7 | interface ProtectedRouteProps { 8 | children: ReactNode; 9 | allowedRoles?: UserRole[]; 10 | } 11 | 12 | export function ProtectedRoute({ children, allowedRoles }: ProtectedRouteProps) { 13 | const { user, isLoading } = useAuthStore(); 14 | const location = useLocation(); 15 | 16 | if (isLoading) { 17 | return ( 18 |
19 |
20 |
21 | ); 22 | } 23 | 24 | // Don't redirect if we're on the register or login pages 25 | if (!user && !location.pathname.match(/^\/(register|login)$/)) { 26 | return ; 27 | } 28 | 29 | if (allowedRoles && user && !allowedRoles.includes(user.role as UserRole)) { 30 | return ; 31 | } 32 | 33 | // Render children AND the modal for authenticated users 34 | return ( 35 | <> 36 | {children} 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /apps/web/src/components/common/DropZone.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, DragEvent, useEffect } from 'react'; 2 | import { cn } from '@/lib/utils'; // Assuming Shadcn's utility for class merging 3 | import { UploadCloud } from 'lucide-react'; 4 | import { platformEventEmitter } from '@paynless/platform'; // Import emitter 5 | 6 | interface DropZoneProps { 7 | className?: string; 8 | children?: React.ReactNode; // Allow passing content inside 9 | activeText?: string; 10 | } 11 | 12 | export const DropZone: React.FC = ({ 13 | className, 14 | children, 15 | activeText = 'Drop file here to import', 16 | }) => { 17 | const [isHovering, setIsHovering] = useState(false); 18 | 19 | // ADDED: useEffect for platform event listener 20 | useEffect(() => { 21 | const handleHover = () => setIsHovering(true); 22 | const handleCancel = () => setIsHovering(false); 23 | 24 | console.log('[DropZone] Subscribing to file drag events'); 25 | platformEventEmitter.on('file-drag-hover', handleHover); 26 | platformEventEmitter.on('file-drag-cancel', handleCancel); 27 | 28 | // Cleanup 29 | return () => { 30 | console.log('[DropZone] Unsubscribing from file drag events'); 31 | platformEventEmitter.off('file-drag-hover', handleHover); 32 | platformEventEmitter.off('file-drag-cancel', handleCancel); 33 | }; 34 | }, []); // Run only once on mount 35 | 36 | return ( 37 |
45 | {isHovering ? ( 46 |
47 | 48 |

{activeText}

49 |
50 | ) : ( 51 | children || ( 52 |
53 | 54 |

Drag and drop file here

55 |

(or use the import button)

56 |
57 | ) 58 | )} 59 |
60 | ); 61 | }; -------------------------------------------------------------------------------- /apps/web/src/components/common/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { Component, ErrorInfo, ReactNode } from 'react'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | fallback?: ReactNode; // Optional custom fallback component 8 | } 9 | 10 | interface State { 11 | hasError: boolean; 12 | error?: Error; 13 | } 14 | 15 | class ErrorBoundary extends Component { 16 | override state: State = { 17 | hasError: false, 18 | }; 19 | 20 | static getDerivedStateFromError(error: Error): State { 21 | // Update state so the next render will show the fallback UI. 22 | return { hasError: true, error }; 23 | } 24 | 25 | public override componentDidCatch(error: Error, errorInfo: ErrorInfo) { 26 | // Log the error assuming logger takes message and context/error object 27 | console.error('ErrorBoundary caught an error:', { error, errorInfo }); 28 | } 29 | 30 | public override render() { 31 | if (this.state.hasError) { 32 | if (this.props.fallback) { 33 | return this.props.fallback; 34 | } 35 | // Default fallback UI 36 | return ( 37 |
38 |

Oops, something went wrong.

39 |

40 | We encountered an error. Please try refreshing the page or contact support if the problem persists. 41 |

42 | {this.state.error && ( 43 |
44 |               {this.state.error.message}
45 |             
46 | )} 47 |
48 | ); 49 | } 50 | 51 | return this.props.children; 52 | } 53 | } 54 | 55 | export default ErrorBoundary; -------------------------------------------------------------------------------- /apps/web/src/components/common/FileDataDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface FileDataDisplayProps { 4 | content: string; 5 | title?: string; 6 | } 7 | 8 | /** 9 | * A component to display string content, typically loaded from a file, 10 | * within a formatted, read-only container. 11 | */ 12 | export const FileDataDisplay: React.FC = ({ content, title }) => { 13 | return ( 14 |
15 | {title && ( 16 |

17 | {title} 18 |

19 | )} 20 |
21 |         {content}
22 |       
23 |
24 | ); 25 | }; -------------------------------------------------------------------------------- /apps/web/src/components/common/MarkdownRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import { Prism } from 'react-syntax-highlighter'; 5 | import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; 6 | 7 | export interface MarkdownRendererProps { 8 | content: string; 9 | className?: string; 10 | } 11 | 12 | export const MarkdownRenderer: React.FC = ({ content, className }) => { 13 | // Basic styling for markdown elements using Tailwind prose classes 14 | const markdownStyles = ` 15 | prose 16 | dark:prose-invert 17 | prose-sm 18 | max-w-none 19 | prose-headings:font-semibold 20 | prose-a:text-blue-600 prose-a:hover:underline 21 | prose-code:bg-gray-200 prose-code:dark:bg-gray-800 prose-code:p-1 prose-code:rounded prose-code:text-sm 22 | prose-pre:bg-gray-200 prose-pre:dark:bg-gray-800 prose-pre:p-2 prose-pre:rounded prose-pre:overflow-x-auto 23 | prose-blockquote:border-l-4 prose-blockquote:pl-4 prose-blockquote:italic 24 | `; 25 | 26 | return ( 27 |
28 | & { inline?: boolean; children?: React.ReactNode }) { 32 | const match = /language-(\w+)/.exec(className || ''); 33 | if (inline || !className) { 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | } 40 | return ( 41 |
42 | 46 | {String(children).replace(/\n$/, '')} 47 | 48 |
49 | ); 50 | }, 51 | }} 52 | > 53 | {content} 54 |
55 |
56 | ); 57 | }; -------------------------------------------------------------------------------- /apps/web/src/components/common/TextInputArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Textarea } from '@/components/ui/textarea'; 3 | import { Label } from '@/components/ui/label'; 4 | 5 | interface TextInputAreaProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | disabled?: boolean; 9 | label: string; 10 | placeholder?: string; 11 | id?: string; 12 | rows?: number; 13 | dataTestId?: string; 14 | } 15 | 16 | /** 17 | * A reusable textarea input component with a label. 18 | * (Refactored from MnemonicInputArea) 19 | */ 20 | export const TextInputArea: React.FC = ({ 21 | value, 22 | onChange, 23 | disabled = false, 24 | label, 25 | placeholder, 26 | id = 'textInputArea', 27 | rows = 4, 28 | dataTestId 29 | }) => { 30 | // Handler to extract value from event and pass to prop function 31 | const handleChange = (event: React.ChangeEvent) => { 32 | onChange(event.target.value); 33 | }; 34 | 35 | return ( 36 |
37 | 38 |