├── .github └── workflows │ ├── docker-build.yml │ └── update-PROD.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── .env.example ├── .github │ └── workflows │ │ ├── bump-version.yml │ │ ├── ci.yml │ │ └── publish.yml ├── .gitignore ├── Dockerfile ├── MANIFEST.in ├── README.md ├── agent │ ├── __init__.py │ ├── agent_builder_prompt.py │ ├── api.py │ ├── gemini_prompt.py │ ├── prompt.py │ ├── run.py │ ├── sample_responses │ │ ├── 1.txt │ │ ├── 2.txt │ │ └── 3.txt │ └── tools │ │ ├── __init__.py │ │ ├── computer_use_tool.py │ │ ├── data_providers │ │ ├── ActiveJobsProvider.py │ │ ├── AmazonProvider.py │ │ ├── LinkedinProvider.py │ │ ├── RapidDataProviderBase.py │ │ ├── TwitterProvider.py │ │ ├── YahooFinanceProvider.py │ │ └── ZillowProvider.py │ │ ├── data_providers_tool.py │ │ ├── expand_msg_tool.py │ │ ├── mcp_tool_wrapper.py │ │ ├── message_tool.py │ │ ├── sb_browser_tool.py │ │ ├── sb_deploy_tool.py │ │ ├── sb_expose_tool.py │ │ ├── sb_files_tool.py │ │ ├── sb_shell_tool.py │ │ ├── sb_vision_tool.py │ │ ├── update_agent_tool.py │ │ └── web_search_tool.py ├── agentpress │ ├── __init__.py │ ├── context_manager.py │ ├── response_processor.py │ ├── thread_manager.py │ ├── tool.py │ ├── tool_registry.py │ ├── utils │ │ ├── __init__.py │ │ └── json_helpers.py │ └── xml_tool_parser.py ├── api.py ├── docker-compose.prod.yml ├── docker-compose.yml ├── docs │ ├── browser_state_image_display.md │ ├── double_escape_fix_summary.md │ └── see_image_compression.md ├── list_mcp.py ├── mcp_local │ ├── __init__.py │ ├── api.py │ └── client.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── run_agent_background.py ├── sandbox │ ├── README.md │ ├── api.py │ ├── docker │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── browser_api.py │ │ ├── docker-compose.yml │ │ ├── entrypoint.sh │ │ ├── requirements.txt │ │ ├── server.py │ │ └── supervisord.conf │ ├── sandbox.py │ └── tool_base.py ├── sentry.py ├── services │ ├── billing.py │ ├── docker │ │ └── redis.conf │ ├── langfuse.py │ ├── llm.py │ ├── mcp_custom.py │ ├── mcp_temp.py │ ├── redis.py │ ├── supabase.py │ └── transcription.py ├── supabase │ ├── .env.example │ ├── .gitignore │ ├── config.toml │ ├── email-template.html │ ├── kong.yml │ └── migrations │ │ ├── 20240414161707_basejump-setup.sql │ │ ├── 20240414161947_basejump-accounts.sql │ │ ├── 20240414162100_basejump-invitations.sql │ │ ├── 20240414162131_basejump-billing.sql │ │ ├── 20250409211903_basejump-configure.sql │ │ ├── 20250409212058_initial.sql │ │ ├── 20250416133920_agentpress_schema.sql │ │ ├── 20250504123828_fix_thread_select_policy.sql │ │ ├── 20250523133848_admin-view-access.sql │ │ ├── 20250524062639_agents_table.sql │ │ ├── 20250529125628_agent_marketplace.sql │ │ ├── 20250601000000_add_thread_metadata.sql │ │ └── 20250602000000_add_custom_mcps_column.sql ├── test_custom_mcp.py ├── test_mcp_use.py └── utils │ ├── __init__.py │ ├── auth_utils.py │ ├── config.py │ ├── constants.py │ ├── files_utils.py │ ├── logger.py │ ├── s3_upload_utils.py │ └── scripts │ ├── archive_inactive_sandboxes.py │ ├── archive_old_sandboxes.py │ ├── delete_user_sandboxes.py │ ├── generate_share_links.py │ ├── set_all_customers_active.py │ └── update_customer_active_status.py ├── docker-compose.yaml ├── docs ├── .DS_Store ├── SELF-HOSTING.md └── images │ ├── architecture_diagram.svg │ └── diagram.png ├── frontend ├── .env.example ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── README.md ├── components.json ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── Frame 50.svg │ ├── banner.png │ ├── favicon.png │ ├── holo.png │ ├── kortix-logo-white.svg │ ├── kortix-logo.svg │ ├── kortix-symbol.svg │ ├── mac.png │ ├── share-page │ │ └── og-fallback.png │ ├── thumbnail-dark.png │ ├── thumbnail-light.png │ └── worldoscollage.mp4 ├── src │ ├── app │ │ ├── (dashboard) │ │ │ ├── (personalAccount) │ │ │ │ ├── loading.tsx │ │ │ │ └── settings │ │ │ │ │ ├── billing │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── teams │ │ │ │ │ └── page.tsx │ │ │ ├── (teamAccount) │ │ │ │ └── [accountSlug] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── settings │ │ │ │ │ ├── billing │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── members │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── agents │ │ │ │ ├── [threadId] │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── redirect-page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── agent-builder-chat.tsx │ │ │ │ │ ├── agent-mcp-configuration.tsx │ │ │ │ │ ├── agent-preview.tsx │ │ │ │ │ ├── agent-tools-configuration.tsx │ │ │ │ │ ├── agents-grid.tsx │ │ │ │ │ ├── agents-list.tsx │ │ │ │ │ ├── create-agent-dialog.tsx │ │ │ │ │ ├── empty-state.tsx │ │ │ │ │ ├── loading-state.tsx │ │ │ │ │ ├── mcp-configuration.tsx │ │ │ │ │ ├── mcp │ │ │ │ │ │ ├── _loaders │ │ │ │ │ │ │ ├── mcp-list-loader.tsx │ │ │ │ │ │ │ └── mcp-search-loader.tsx │ │ │ │ │ │ ├── browse-dialog.tsx │ │ │ │ │ │ ├── categorized-servers.tsx │ │ │ │ │ │ ├── category-sidebar.tsx │ │ │ │ │ │ ├── config-dialog.tsx │ │ │ │ │ │ ├── configured-mcp-list.tsx │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ ├── custom-mcp-dialog.tsx │ │ │ │ │ │ ├── mcp-configuration-new.tsx │ │ │ │ │ │ ├── mcp-server-card.tsx │ │ │ │ │ │ ├── search-results.tsx │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── pagination.tsx │ │ │ │ │ ├── results-info.tsx │ │ │ │ │ ├── search-and-filters.tsx │ │ │ │ │ ├── style-picker.tsx │ │ │ │ │ └── update-agent-dialog.tsx │ │ │ │ ├── _data │ │ │ │ │ └── tools.ts │ │ │ │ ├── _hooks │ │ │ │ │ └── use-agents-filtering.ts │ │ │ │ ├── _types │ │ │ │ │ └── index.ts │ │ │ │ ├── _utils │ │ │ │ │ └── get-agent-style.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── new │ │ │ │ │ └── [agentId] │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── _components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── suggestions │ │ │ │ │ │ ├── example-prompts.tsx │ │ │ │ │ │ └── examples.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── marketplace │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ └── projects │ │ │ │ └── [projectId] │ │ │ │ └── thread │ │ │ │ ├── [threadId] │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ ├── ThreadError.tsx │ │ │ │ ├── ThreadLayout.tsx │ │ │ │ ├── UpgradeDialog.tsx │ │ │ │ └── index.ts │ │ │ │ ├── _hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useBilling.ts │ │ │ │ ├── useKeyboardShortcuts.ts │ │ │ │ ├── useThreadData.ts │ │ │ │ └── useToolCalls.ts │ │ │ │ └── _types │ │ │ │ └── index.ts │ │ ├── (home) │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── api │ │ │ └── share-page │ │ │ │ └── og-image │ │ │ │ └── route.tsx │ │ ├── auth │ │ │ ├── actions.ts │ │ │ ├── callback │ │ │ │ └── route.ts │ │ │ ├── page.tsx │ │ │ └── reset-password │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── global-error.tsx │ │ ├── globals.css │ │ ├── invitation │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── legal │ │ │ └── page.tsx │ │ ├── metadata.ts │ │ ├── monitoring │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── opengraph-image.tsx │ │ ├── providers.tsx │ │ └── share │ │ │ └── [threadId] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── components │ │ ├── AuthProvider.tsx │ │ ├── GoogleSignIn.tsx │ │ ├── basejump │ │ │ ├── accept-team-invitation.tsx │ │ │ ├── account-selector.tsx │ │ │ ├── client-user-account-button.tsx │ │ │ ├── create-team-dialog.tsx │ │ │ ├── create-team-invitation-button.tsx │ │ │ ├── delete-team-invitation-button.tsx │ │ │ ├── delete-team-member-form.tsx │ │ │ ├── edit-personal-account-name.tsx │ │ │ ├── edit-team-member-role-form.tsx │ │ │ ├── edit-team-name.tsx │ │ │ ├── edit-team-slug.tsx │ │ │ ├── manage-team-invitations.tsx │ │ │ ├── manage-team-members.tsx │ │ │ ├── manage-teams.tsx │ │ │ ├── new-invitation-form.tsx │ │ │ ├── new-team-form.tsx │ │ │ ├── team-member-options.tsx │ │ │ └── user-account-button.tsx │ │ ├── billing │ │ │ ├── account-billing-status.tsx │ │ │ ├── billing-modal.tsx │ │ │ ├── payment-required-dialog.tsx │ │ │ └── usage-limit-alert.tsx │ │ ├── dashboard │ │ │ └── agent-selector.tsx │ │ ├── examples │ │ │ └── ErrorHandlingDemo.tsx │ │ ├── file-renderers │ │ │ ├── binary-renderer.tsx │ │ │ ├── code-renderer.tsx │ │ │ ├── csv-renderer.tsx │ │ │ ├── html-renderer.tsx │ │ │ ├── image-renderer.tsx │ │ │ ├── index.tsx │ │ │ ├── markdown-renderer.tsx │ │ │ └── pdf-renderer.tsx │ │ ├── home │ │ │ ├── first-bento-animation.tsx │ │ │ ├── fourth-bento-animation.tsx │ │ │ ├── icons.tsx │ │ │ ├── nav-menu.tsx │ │ │ ├── second-bento-animation.tsx │ │ │ ├── section-header.tsx │ │ │ ├── sections │ │ │ │ ├── bento-section.tsx │ │ │ │ ├── company-showcase.tsx │ │ │ │ ├── cta-section.tsx │ │ │ │ ├── faq-section.tsx │ │ │ │ ├── feature-section.tsx │ │ │ │ ├── footer-section.tsx │ │ │ │ ├── growth-section.tsx │ │ │ │ ├── hero-section.tsx │ │ │ │ ├── hero-video-section.tsx │ │ │ │ ├── navbar.tsx │ │ │ │ ├── open-source-section.tsx │ │ │ │ ├── pricing-section.tsx │ │ │ │ ├── quote-section.tsx │ │ │ │ ├── testimonial-section.tsx │ │ │ │ └── use-cases-section.tsx │ │ │ ├── testimonial-scroll.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-toggle.tsx │ │ │ ├── third-bento-animation.tsx │ │ │ └── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── feature-slideshow.tsx │ │ │ │ ├── flickering-grid.tsx │ │ │ │ ├── globe.tsx │ │ │ │ ├── hero-video-dialog.tsx │ │ │ │ ├── marquee.tsx │ │ │ │ ├── orbiting-circle.tsx │ │ │ │ ├── reasoning.tsx │ │ │ │ └── response-stream.tsx │ │ ├── maintenance-alert.tsx │ │ ├── maintenance │ │ │ └── maintenance-page.tsx │ │ ├── payment │ │ │ └── paywall-dialog.tsx │ │ ├── sentry │ │ │ └── index.tsx │ │ ├── sidebar │ │ │ ├── cta.tsx │ │ │ ├── date-picker.tsx │ │ │ ├── kortix-enterprise-modal.tsx │ │ │ ├── kortix-logo.tsx │ │ │ ├── nav-agents.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── nav-user-with-teams.tsx │ │ │ ├── search-search.tsx │ │ │ ├── share-modal.tsx │ │ │ └── sidebar-left.tsx │ │ ├── theme-provider.tsx │ │ ├── thread │ │ │ ├── DeleteConfirmationDialog.tsx │ │ │ ├── attachment-group.tsx │ │ │ ├── chat-input │ │ │ │ ├── _use-model-selection.ts │ │ │ │ ├── agent-selector.tsx │ │ │ │ ├── chat-input.tsx │ │ │ │ ├── custom-model-dialog.tsx │ │ │ │ ├── file-upload-handler.tsx │ │ │ │ ├── message-input.tsx │ │ │ │ ├── model-selector.tsx │ │ │ │ ├── uploaded-file-display.tsx │ │ │ │ └── voice-recorder.tsx │ │ │ ├── content │ │ │ │ ├── PlaybackControls.tsx │ │ │ │ ├── ThreadContent.tsx │ │ │ │ ├── ThreadSkeleton.tsx │ │ │ │ └── loader.tsx │ │ │ ├── file-attachment.tsx │ │ │ ├── file-browser.tsx │ │ │ ├── file-viewer-modal.tsx │ │ │ ├── preview-renderers │ │ │ │ ├── csv-renderer.tsx │ │ │ │ ├── html-renderer.tsx │ │ │ │ ├── index.ts │ │ │ │ └── markdown-renderer.tsx │ │ │ ├── thread-site-header.tsx │ │ │ ├── tool-call-side-panel.tsx │ │ │ ├── tool-views │ │ │ │ ├── BrowserToolView.tsx │ │ │ │ ├── CompleteToolView.tsx │ │ │ │ ├── GenericToolView.tsx │ │ │ │ ├── WebCrawlToolView.tsx │ │ │ │ ├── ask-tool │ │ │ │ │ ├── AskToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── command-tool │ │ │ │ │ ├── CommandToolView.tsx │ │ │ │ │ ├── TerminateCommandToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── data-provider-tool │ │ │ │ │ ├── DataProviderEndpointsToolView.tsx │ │ │ │ │ ├── ExecuteDataProviderCallToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── expose-port-tool │ │ │ │ │ ├── ExposePortToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── file-operation │ │ │ │ │ ├── FileOperationToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── mcp-content-renderer.tsx │ │ │ │ ├── mcp-format-detector.ts │ │ │ │ ├── mcp-tool │ │ │ │ │ ├── McpToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── see-image-tool │ │ │ │ │ ├── SeeImageToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── shared │ │ │ │ │ ├── ImageLoader.tsx │ │ │ │ │ └── LoadingState.tsx │ │ │ │ ├── str-replace │ │ │ │ │ ├── StrReplaceToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── tool-result-parser.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── web-scrape-tool │ │ │ │ │ ├── WebScrapeToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── web-search-tool │ │ │ │ │ ├── WebSearchToolView.tsx │ │ │ │ │ └── _utils.ts │ │ │ │ ├── wrapper │ │ │ │ │ ├── ToolViewRegistry.tsx │ │ │ │ │ ├── ToolViewWrapper.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── xml-parser.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── code-block.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── editable.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── markdown.tsx │ │ │ ├── popover.tsx │ │ │ ├── portal.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── status-overlay.tsx │ │ │ ├── submit-button.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ ├── contexts │ │ ├── BillingContext.tsx │ │ └── DeleteOperationContext.tsx │ ├── hooks │ │ ├── react-query │ │ │ ├── agents │ │ │ │ ├── keys.ts │ │ │ │ ├── use-agents.ts │ │ │ │ └── utils.ts │ │ │ ├── dashboard │ │ │ │ ├── keys.ts │ │ │ │ ├── use-initiate-agent.ts │ │ │ │ └── utils.ts │ │ │ ├── files │ │ │ │ ├── index.ts │ │ │ │ ├── keys.ts │ │ │ │ ├── use-file-content.ts │ │ │ │ ├── use-file-mutations.ts │ │ │ │ ├── use-file-queries.ts │ │ │ │ ├── use-image-content.ts │ │ │ │ └── use-sandbox-mutations.ts │ │ │ ├── index.ts │ │ │ ├── marketplace │ │ │ │ └── use-marketplace.ts │ │ │ ├── mcp │ │ │ │ └── use-mcp-servers.ts │ │ │ ├── sidebar │ │ │ │ ├── keys.ts │ │ │ │ ├── use-project-mutations.ts │ │ │ │ ├── use-public-projects.ts │ │ │ │ └── use-sidebar.ts │ │ │ ├── subscriptions │ │ │ │ ├── keys.ts │ │ │ │ ├── use-billing.ts │ │ │ │ ├── use-model.ts │ │ │ │ └── use-subscriptions.ts │ │ │ ├── threads │ │ │ │ ├── keys.ts │ │ │ │ ├── use-agent-run.ts │ │ │ │ ├── use-billing-status.ts │ │ │ │ ├── use-messages.ts │ │ │ │ ├── use-project.ts │ │ │ │ ├── use-thread-mutations.ts │ │ │ │ ├── use-thread-queries.ts │ │ │ │ ├── use-threads.ts │ │ │ │ └── utils.ts │ │ │ ├── transcription │ │ │ │ └── use-transcription.ts │ │ │ └── usage │ │ │ │ └── use-health.ts │ │ ├── use-accounts.ts │ │ ├── use-announcement-store.ts │ │ ├── use-cached-file.ts │ │ ├── use-feature-alerts.ts │ │ ├── use-file-content.ts │ │ ├── use-image-content.ts │ │ ├── use-media-query.ts │ │ ├── use-mobile.ts │ │ ├── use-modal-store.ts │ │ ├── use-query.ts │ │ ├── useAgentStream.ts │ │ ├── useBillingError.ts │ │ └── useVncPreloader.ts │ ├── instrumentation-client.ts │ ├── instrumentation.ts │ ├── lib │ │ ├── actions │ │ │ ├── invitations.ts │ │ │ ├── members.ts │ │ │ ├── personal-account.ts │ │ │ ├── teams.ts │ │ │ └── threads.ts │ │ ├── api-client.ts │ │ ├── api-enhanced.ts │ │ ├── api-server.ts │ │ ├── api.ts │ │ ├── cache-init.ts │ │ ├── config.ts │ │ ├── constants │ │ │ └── errorCodeMessages.ts │ │ ├── error-handler.ts │ │ ├── full-invitation-url.ts │ │ ├── home.tsx │ │ ├── site.ts │ │ ├── supabase │ │ │ ├── client.ts │ │ │ ├── handle-edge-error.ts │ │ │ ├── middleware.ts │ │ │ └── server.ts │ │ ├── utils.ts │ │ └── utils │ │ │ ├── dirty-string-parser.ts │ │ │ ├── tool-parser.ts │ │ │ └── url.ts │ ├── providers │ │ ├── modal-providers.tsx │ │ └── react-query-provider.tsx │ └── sentry.config.ts ├── test-new-format.js └── tsconfig.json ├── mise.toml ├── setup.py ├── start.py ├── test.json └── test_agent_builder_response.py /.github/workflows/update-PROD.yml: -------------------------------------------------------------------------------- 1 | name: Update PRODUCTION Branch 2 | on: 3 | workflow_dispatch: 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | update-production: 10 | name: Rebase PRODUCTION to main 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | - name: Configure Git 18 | run: | 19 | git config user.name "GitHub Actions" 20 | git config user.email "actions@github.com" 21 | - name: Rebase PRODUCTION 22 | run: | 23 | git checkout PRODUCTION 24 | git rebase origin/main 25 | git push origin PRODUCTION --force 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Suna 2 | 3 | Thank you for your interest in contributing to Suna! This document outlines the contribution process and guidelines. 4 | 5 | ## Contribution Workflow 6 | 7 | 1. Fork the repository 8 | 2. Create a feature branch (`git checkout -b feature/your-feature`) 9 | 3. Commit your changes (`git commit -am 'feat(your_file): add some feature'`) 10 | 4. Push to the branch (`git push origin feature/your-feature`) 11 | 5. Open a Pull Request 12 | 13 | ## Development Setup 14 | 15 | For detailed setup instructions, please refer to: 16 | 17 | - [Backend Development Setup](backend/README.md) 18 | - [Frontend Development Setup](frontend/README.md) 19 | 20 | ## Code Style Guidelines 21 | 22 | - Follow existing code style and patterns 23 | - Use descriptive commit messages 24 | - Keep PRs focused on a single feature or fix 25 | 26 | ## Reporting Issues 27 | 28 | When reporting issues, please include: 29 | 30 | - Steps to reproduce 31 | - Expected behavior 32 | - Actual behavior 33 | - Environment details (OS, Node/Docker versions, etc.) 34 | - Relevant logs or screenshots 35 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and fill in your values 2 | 3 | # Environment Mode 4 | # Valid values: local, staging, production 5 | ENV_MODE=local 6 | 7 | #DATABASE 8 | SUPABASE_URL= 9 | SUPABASE_ANON_KEY= 10 | SUPABASE_SERVICE_ROLE_KEY= 11 | 12 | REDIS_HOST=redis 13 | REDIS_PORT=6379 14 | REDIS_PASSWORD= 15 | REDIS_SSL=false 16 | 17 | RABBITMQ_HOST=rabbitmq 18 | RABBITMQ_PORT=5672 19 | 20 | # LLM Providers: 21 | ANTHROPIC_API_KEY= 22 | OPENAI_API_KEY= 23 | MODEL_TO_USE= 24 | 25 | AWS_ACCESS_KEY_ID= 26 | AWS_SECRET_ACCESS_KEY= 27 | AWS_REGION_NAME= 28 | 29 | GROQ_API_KEY= 30 | OPENROUTER_API_KEY= 31 | 32 | # DATA APIS 33 | RAPID_API_KEY= 34 | 35 | # WEB SEARCH 36 | TAVILY_API_KEY= 37 | 38 | # WEB SCRAPE 39 | FIRECRAWL_API_KEY= 40 | FIRECRAWL_URL= 41 | 42 | # Sandbox container provider: 43 | DAYTONA_API_KEY= 44 | DAYTONA_SERVER_URL= 45 | DAYTONA_TARGET= 46 | 47 | LANGFUSE_PUBLIC_KEY="pk-REDACTED" 48 | LANGFUSE_SECRET_KEY="sk-REDACTED" 49 | LANGFUSE_HOST="https://cloud.langfuse.com" 50 | 51 | SMITHERY_API_KEY= -------------------------------------------------------------------------------- /backend/.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_part: 7 | description: 'Part of version to bump (major, minor, patch)' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - major 13 | - minor 14 | - patch 15 | 16 | # Add these permissions 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | bump-version: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: '3.12' 33 | 34 | - name: Install Poetry 35 | run: | 36 | curl -sSL https://install.python-poetry.org | python3 - 37 | 38 | - name: Configure Git 39 | run: | 40 | git config --global user.name 'github-actions[bot]' 41 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 42 | 43 | - name: Bump version 44 | run: | 45 | poetry version ${{ github.event.inputs.version_part }} 46 | NEW_VERSION=$(poetry version -s) 47 | echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV 48 | 49 | - name: Create Pull Request 50 | uses: peter-evans/create-pull-request@v5 51 | with: 52 | commit-message: "chore: bump version to ${{ env.NEW_VERSION }}" 53 | title: "Bump version to ${{ env.NEW_VERSION }}" 54 | body: "Automated version bump to ${{ env.NEW_VERSION }}" 55 | branch: "bump-version-${{ env.NEW_VERSION }}" 56 | base: "main" -------------------------------------------------------------------------------- /backend/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.12' 19 | 20 | - name: Install Poetry 21 | run: | 22 | curl -sSL https://install.python-poetry.org | python3 - 23 | 24 | - name: Update lock file and install dependencies 25 | run: | 26 | poetry lock 27 | poetry install -------------------------------------------------------------------------------- /backend/.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | # Allows manual trigger from GitHub Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.12' 20 | 21 | - name: Install Poetry 22 | run: | 23 | curl -sSL https://install.python-poetry.org | python3 - 24 | 25 | - name: Configure Poetry 26 | run: | 27 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 28 | 29 | - name: Build package 30 | run: poetry build 31 | 32 | - name: Publish to PyPI 33 | run: poetry publish -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | # Set environment variables 4 | ENV PYTHONUNBUFFERED=1 \ 5 | PYTHONDONTWRITEBYTECODE=1 \ 6 | ENV_MODE="production" \ 7 | PYTHONPATH=/app 8 | 9 | WORKDIR /app 10 | 11 | # Install system dependencies 12 | RUN apt-get update && apt-get install -y --no-install-recommends \ 13 | build-essential \ 14 | curl \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # Create non-root user and set up directories 18 | RUN useradd -m -u 1000 appuser && \ 19 | mkdir -p /app/logs && \ 20 | chown -R appuser:appuser /app 21 | 22 | # Install Python dependencies 23 | COPY --chown=appuser:appuser requirements.txt . 24 | RUN pip install --no-cache-dir -r requirements.txt gunicorn 25 | 26 | # Switch to non-root user 27 | USER appuser 28 | 29 | # Copy application code 30 | COPY --chown=appuser:appuser . . 31 | 32 | # Expose the port the app runs on 33 | EXPOSE 8000 34 | 35 | # Calculate optimal worker count based on 16 vCPUs 36 | # Using (2*CPU)+1 formula for CPU-bound applications 37 | ENV WORKERS=33 38 | ENV THREADS=2 39 | ENV WORKER_CONNECTIONS=2000 40 | 41 | EXPOSE 8000 42 | 43 | # Gunicorn configuration 44 | CMD ["sh", "-c", "gunicorn api:app \ 45 | --workers $WORKERS \ 46 | --worker-class uvicorn.workers.UvicornWorker \ 47 | --bind 0.0.0.0:8000 \ 48 | --timeout 1800 \ 49 | --graceful-timeout 600 \ 50 | --keep-alive 1800 \ 51 | --max-requests 0 \ 52 | --max-requests-jitter 0 \ 53 | --forwarded-allow-ips '*' \ 54 | --worker-connections $WORKER_CONNECTIONS \ 55 | --worker-tmp-dir /dev/shm \ 56 | --preload \ 57 | --log-level info \ 58 | --access-logfile - \ 59 | --error-logfile - \ 60 | --capture-output \ 61 | --enable-stdio-inheritance \ 62 | --threads $THREADS"] 63 | -------------------------------------------------------------------------------- /backend/MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include all Python files in agentpress directory 2 | recursive-include agentpress *.py 3 | 4 | # Include example files 5 | recursive-include agentpress/examples * 6 | 7 | # Include any other necessary files 8 | include LICENSE 9 | include README.md 10 | include pyproject.toml 11 | 12 | # Exclude unnecessary files 13 | global-exclude *.pyc 14 | global-exclude __pycache__ 15 | global-exclude .DS_Store 16 | global-exclude *.pyo 17 | global-exclude *.pyd -------------------------------------------------------------------------------- /backend/agent/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility functions and constants for agent tools -------------------------------------------------------------------------------- /backend/agent/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility functions and constants for agent tools -------------------------------------------------------------------------------- /backend/agent/tools/data_providers/RapidDataProviderBase.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from typing import Dict, Any, Optional, TypedDict, Literal 4 | 5 | 6 | class EndpointSchema(TypedDict): 7 | route: str 8 | method: Literal['GET', 'POST'] 9 | name: str 10 | description: str 11 | payload: Dict[str, Any] 12 | 13 | 14 | class RapidDataProviderBase: 15 | def __init__(self, base_url: str, endpoints: Dict[str, EndpointSchema]): 16 | self.base_url = base_url 17 | self.endpoints = endpoints 18 | 19 | def get_endpoints(self): 20 | return self.endpoints 21 | 22 | def call_endpoint( 23 | self, 24 | route: str, 25 | payload: Optional[Dict[str, Any]] = None 26 | ): 27 | """ 28 | Call an API endpoint with the given parameters and data. 29 | 30 | Args: 31 | endpoint (EndpointSchema): The endpoint configuration dictionary 32 | params (dict, optional): Query parameters for GET requests 33 | payload (dict, optional): JSON payload for POST requests 34 | 35 | Returns: 36 | dict: The JSON response from the API 37 | """ 38 | if route.startswith("/"): 39 | route = route[1:] 40 | 41 | endpoint = self.endpoints.get(route) 42 | if not endpoint: 43 | raise ValueError(f"Endpoint {route} not found") 44 | 45 | url = f"{self.base_url}{endpoint['route']}" 46 | 47 | headers = { 48 | "x-rapidapi-key": os.getenv("RAPID_API_KEY"), 49 | "x-rapidapi-host": url.split("//")[1].split("/")[0], 50 | "Content-Type": "application/json" 51 | } 52 | 53 | method = endpoint.get('method', 'GET').upper() 54 | 55 | if method == 'GET': 56 | response = requests.get(url, params=payload, headers=headers) 57 | elif method == 'POST': 58 | response = requests.post(url, json=payload, headers=headers) 59 | else: 60 | raise ValueError(f"Unsupported HTTP method: {method}") 61 | return response.json() 62 | -------------------------------------------------------------------------------- /backend/agentpress/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility functions and constants for agent tools -------------------------------------------------------------------------------- /backend/agentpress/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Utils module for AgentPress -------------------------------------------------------------------------------- /backend/docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | deploy: 4 | resources: 5 | limits: 6 | cpus: "14" 7 | memory: 48G 8 | reservations: 9 | cpus: "8" 10 | memory: 32G 11 | 12 | worker: 13 | command: python -m dramatiq --processes 20 --threads 16 run_agent_background 14 | deploy: 15 | resources: 16 | limits: 17 | cpus: "14" 18 | memory: 48G 19 | reservations: 20 | cpus: "8" 21 | memory: 32G 22 | 23 | redis: 24 | deploy: 25 | resources: 26 | limits: 27 | cpus: "2" 28 | memory: 12G 29 | reservations: 30 | cpus: "1" 31 | memory: 8G 32 | 33 | rabbitmq: 34 | deploy: 35 | resources: 36 | limits: 37 | cpus: "2" 38 | memory: 12G 39 | reservations: 40 | cpus: "1" 41 | memory: 8G 42 | -------------------------------------------------------------------------------- /backend/mcp_local/__init__.py: -------------------------------------------------------------------------------- 1 | # Local MCP (Model Context Protocol) integration module -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "suna" 3 | version = "1.0" 4 | description = "open source generalist AI Agent" 5 | authors = ["marko-kraemer "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://www.suna.so/" 9 | repository = "https://github.com/kortix-ai/suna" 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.12", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | ] 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.11" 21 | python-dotenv = "1.0.1" 22 | litellm = "1.66.1" 23 | click = "8.1.7" 24 | questionary = "2.0.1" 25 | requests = "^2.31.0" 26 | packaging = "24.1" 27 | setuptools = "75.3.0" 28 | pytest = "8.3.3" 29 | pytest-asyncio = "0.24.0" 30 | asyncio = "3.4.3" 31 | altair = "4.2.2" 32 | prisma = "0.15.0" 33 | fastapi = "0.110.0" 34 | uvicorn = "0.27.1" 35 | python-multipart = "0.0.20" 36 | redis = "5.2.1" 37 | upstash-redis = "1.3.0" 38 | supabase = "^2.15.0" 39 | pyjwt = "2.10.1" 40 | exa-py = "^1.9.1" 41 | e2b-code-interpreter = "^1.2.0" 42 | certifi = "2024.2.2" 43 | python-ripgrep = "0.0.6" 44 | daytona_sdk = "^0.14.0" 45 | boto3 = "^1.34.0" 46 | openai = "^1.72.0" 47 | nest-asyncio = "^1.6.0" 48 | vncdotool = "^1.2.0" 49 | tavily-python = "^0.5.4" 50 | pytesseract = "^0.3.13" 51 | stripe = "^12.0.1" 52 | dramatiq = "^1.17.1" 53 | pika = "^1.3.2" 54 | prometheus-client = "^0.21.1" 55 | langfuse = "^2.60.5" 56 | Pillow = "^10.0.0" 57 | mcp = "^1.0.0" 58 | sentry-sdk = {extras = ["fastapi"], version = "^2.29.1"} 59 | 60 | [tool.poetry.scripts] 61 | agentpress = "agentpress.cli:main" 62 | 63 | [[tool.poetry.packages]] 64 | include = "agentpress" 65 | 66 | [tool.poetry.group.dev.dependencies] 67 | daytona-sdk = "^0.14.0" 68 | 69 | [build-system] 70 | requires = ["poetry-core"] 71 | build-backend = "poetry.core.masonry.api" 72 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.0.1 2 | litellm==1.66.1 3 | click==8.1.7 4 | questionary==2.0.1 5 | requests>=2.31.0 6 | packaging==24.1 7 | setuptools==75.3.0 8 | pytest==8.3.3 9 | pytest-asyncio==0.24.0 10 | asyncio==3.4.3 11 | altair==4.2.2 12 | prisma==0.15.0 13 | fastapi==0.110.0 14 | uvicorn==0.27.1 15 | python-multipart==0.0.20 16 | redis==5.2.1 17 | upstash-redis==1.3.0 18 | supabase>=2.15.0 19 | pyjwt==2.10.1 20 | exa-py>=1.9.1 21 | e2b-code-interpreter>=1.2.0 22 | certifi==2024.2.2 23 | python-ripgrep==0.0.6 24 | daytona_sdk==0.14.0 25 | boto3>=1.34.0 26 | openai>=1.72.0 27 | nest-asyncio>=1.6.0 28 | vncdotool>=1.2.0 29 | tavily-python>=0.5.4 30 | pytesseract==0.3.13 31 | stripe>=12.0.1 32 | dramatiq>=1.17.1 33 | pika>=1.3.2 34 | prometheus-client>=0.21.1 35 | langfuse>=2.60.5 36 | httpx>=0.24.0 37 | Pillow>=10.0.0 38 | sentry-sdk[fastapi]>=2.29.1 39 | mcp>=1.0.0 40 | mcp_use>=1.0.0 41 | aiohttp>=3.9.0 -------------------------------------------------------------------------------- /backend/sandbox/README.md: -------------------------------------------------------------------------------- 1 | # Agent Sandbox 2 | 3 | This directory contains the agent sandbox implementation - a Docker-based virtual environment that agents use as their own computer to execute tasks, access the web, and manipulate files. 4 | 5 | ## Overview 6 | 7 | The sandbox provides a complete containerized Linux environment with: 8 | - Chrome browser for web interactions 9 | - VNC server for accessing the Web User 10 | - Web server for serving content (port 8080) -> loading html files from the /workspace directory 11 | - Full file system access 12 | - Full sudo access 13 | 14 | ## Customizing the Sandbox 15 | 16 | You can modify the sandbox environment for development or to add new capabilities: 17 | 18 | 1. Edit files in the `docker/` directory 19 | 2. Build a custom image: 20 | ``` 21 | cd backend/sandbox/docker 22 | docker compose build 23 | docker push kortix/suna:0.1.2 24 | ``` 25 | 3. Test your changes locally using docker-compose 26 | 27 | ## Using a Custom Image 28 | 29 | To use your custom sandbox image: 30 | 31 | 1. Change the `image` parameter in `docker-compose.yml` (that defines the image name `kortix/suna:___`) 32 | 2. Update the same image name in `backend/sandbox/sandbox.py` in the `create_sandbox` function 33 | 3. If using Daytona for deployment, update the image reference there as well 34 | 35 | ## Publishing New Versions 36 | 37 | When publishing a new version of the sandbox: 38 | 39 | 1. Update the version number in `docker-compose.yml` (e.g., from `0.1.2` to `0.1.3`) 40 | 2. Build the new image: `docker compose build` 41 | 3. Push the new version: `docker push kortix/suna:0.1.3` 42 | 4. Update all references to the image version in: 43 | - `backend/utils/config.py` 44 | - Daytona images 45 | - Any other services using this image -------------------------------------------------------------------------------- /backend/sandbox/docker/README.md: -------------------------------------------------------------------------------- 1 | # Sandbox 2 | -------------------------------------------------------------------------------- /backend/sandbox/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | kortix-suna: 3 | platform: linux/amd64 4 | build: 5 | context: . 6 | dockerfile: ${DOCKERFILE:-Dockerfile} 7 | args: 8 | TARGETPLATFORM: ${TARGETPLATFORM:-linux/amd64} 9 | image: kortix/suna:0.1.2.8 10 | ports: 11 | - "6080:6080" # noVNC web interface 12 | - "5901:5901" # VNC port 13 | - "9222:9222" # Chrome remote debugging port 14 | - "8000:8000" # API server port 15 | - "8080:8080" # HTTP server port 16 | environment: 17 | - ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-false} 18 | - CHROME_PATH=/usr/bin/google-chrome 19 | - CHROME_USER_DATA=/app/data/chrome_data 20 | - CHROME_PERSISTENT_SESSION=${CHROME_PERSISTENT_SESSION:-false} 21 | - CHROME_CDP=${CHROME_CDP:-http://localhost:9222} 22 | - DISPLAY=:99 23 | - PLAYWRIGHT_BROWSERS_PATH=/ms-playwright 24 | - RESOLUTION=${RESOLUTION:-1024x768x24} 25 | - RESOLUTION_WIDTH=${RESOLUTION_WIDTH:-1024} 26 | - RESOLUTION_HEIGHT=${RESOLUTION_HEIGHT:-768} 27 | - VNC_PASSWORD=${VNC_PASSWORD:-vncpassword} 28 | - CHROME_DEBUGGING_PORT=9222 29 | - CHROME_DEBUGGING_HOST=localhost 30 | - CHROME_FLAGS=${CHROME_FLAGS:-"--single-process --no-first-run --no-default-browser-check --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-component-extensions-with-background-pages --disable-dev-shm-usage --disable-extensions --disable-features=TranslateUI --disable-ipc-flooding-protection --disable-renderer-backgrounding --enable-features=NetworkServiceInProcess2 --force-color-profile=srgb --metrics-recording-only --mute-audio --no-sandbox --disable-gpu"} 31 | volumes: 32 | - /tmp/.X11-unix:/tmp/.X11-unix 33 | restart: unless-stopped 34 | shm_size: '2gb' 35 | cap_add: 36 | - SYS_ADMIN 37 | security_opt: 38 | - seccomp=unconfined 39 | tmpfs: 40 | - /tmp 41 | healthcheck: 42 | test: ["CMD", "nc", "-z", "localhost", "5901"] 43 | interval: 10s 44 | timeout: 5s 45 | retries: 3 46 | -------------------------------------------------------------------------------- /backend/sandbox/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start supervisord in the foreground to properly manage child processes 4 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf -------------------------------------------------------------------------------- /backend/sandbox/docker/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.12 2 | uvicorn==0.34.0 3 | pyautogui==0.9.54 4 | pillow==10.2.0 5 | pydantic==2.6.1 6 | pytesseract==0.3.13 -------------------------------------------------------------------------------- /backend/sandbox/docker/server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.staticfiles import StaticFiles 3 | from starlette.middleware.base import BaseHTTPMiddleware 4 | import uvicorn 5 | import os 6 | 7 | # Ensure we're serving from the /workspace directory 8 | workspace_dir = "/workspace" 9 | 10 | class WorkspaceDirMiddleware(BaseHTTPMiddleware): 11 | async def dispatch(self, request: Request, call_next): 12 | # Check if workspace directory exists and recreate if deleted 13 | if not os.path.exists(workspace_dir): 14 | print(f"Workspace directory {workspace_dir} not found, recreating...") 15 | os.makedirs(workspace_dir, exist_ok=True) 16 | return await call_next(request) 17 | 18 | app = FastAPI() 19 | app.add_middleware(WorkspaceDirMiddleware) 20 | 21 | # Initial directory creation 22 | os.makedirs(workspace_dir, exist_ok=True) 23 | app.mount('/', StaticFiles(directory=workspace_dir, html=True), name='site') 24 | 25 | # This is needed for the import string approach with uvicorn 26 | if __name__ == '__main__': 27 | print(f"Starting server with auto-reload, serving files from: {workspace_dir}") 28 | # Don't use reload directly in the run call 29 | uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=True) -------------------------------------------------------------------------------- /backend/sentry.py: -------------------------------------------------------------------------------- 1 | import sentry_sdk 2 | from sentry_sdk.integrations.dramatiq import DramatiqIntegration 3 | import os 4 | 5 | sentry_dsn = os.getenv("SENTRY_DSN", None) 6 | if sentry_dsn: 7 | sentry_sdk.init( 8 | dsn=sentry_dsn, 9 | integrations=[DramatiqIntegration()], 10 | traces_sample_rate=0.1, 11 | send_default_pii=True, 12 | _experiments={ 13 | "enable_logs": True, 14 | }, 15 | ) 16 | 17 | sentry = sentry_sdk 18 | -------------------------------------------------------------------------------- /backend/services/docker/redis.conf: -------------------------------------------------------------------------------- 1 | timeout 120 2 | -------------------------------------------------------------------------------- /backend/services/langfuse.py: -------------------------------------------------------------------------------- 1 | import os 2 | from langfuse import Langfuse 3 | 4 | public_key = os.getenv("LANGFUSE_PUBLIC_KEY") 5 | secret_key = os.getenv("LANGFUSE_SECRET_KEY") 6 | host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com") 7 | 8 | enabled = False 9 | if public_key and secret_key: 10 | enabled = True 11 | 12 | langfuse = Langfuse(enabled=enabled) 13 | -------------------------------------------------------------------------------- /backend/supabase/.env.example: -------------------------------------------------------------------------------- 1 | # If you're using stripe, replace this with your keys 2 | STRIPE_API_KEY=sk_test_asdf 3 | STRIPE_WEBHOOK_SIGNING_SECRET=whsec_asdf 4 | STRIPE_DEFAULT_PLAN_ID=price_asdf 5 | # this is the number of days that will be given to users for trialing 6 | STRIPE_DEFAULT_TRIAL_DAYS=30 7 | 8 | # The allowed host determines what hostnames are allowed to be used for return URLs back from the Stripe billing portal 9 | # If you need to add multiple you can add them directly in the billing-functions function 10 | ALLOWED_HOST=http://localhost:3000 -------------------------------------------------------------------------------- /backend/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /backend/supabase/kong.yml: -------------------------------------------------------------------------------- 1 | _format_version: "2.1" 2 | 3 | _transform: true 4 | 5 | services: 6 | - name: postgrest 7 | url: http://supabase-db:5432 8 | routes: 9 | - name: postgrest-route 10 | paths: 11 | - /rest/v1 12 | plugins: 13 | - name: cors 14 | - name: key-auth 15 | config: 16 | hide_credentials: true 17 | 18 | - name: auth 19 | url: http://supabase-db:5432 20 | routes: 21 | - name: auth-route 22 | paths: 23 | - /auth/v1 24 | plugins: 25 | - name: cors 26 | - name: key-auth 27 | config: 28 | hide_credentials: true 29 | 30 | - name: storage 31 | url: http://supabase-db:5432 32 | routes: 33 | - name: storage-route 34 | paths: 35 | - /storage/v1 36 | plugins: 37 | - name: cors 38 | - name: key-auth 39 | config: 40 | hide_credentials: true 41 | -------------------------------------------------------------------------------- /backend/supabase/migrations/20250409211903_basejump-configure.sql: -------------------------------------------------------------------------------- 1 | UPDATE basejump.config SET enable_team_accounts = TRUE; 2 | UPDATE basejump.config SET enable_personal_account_billing = TRUE; 3 | UPDATE basejump.config SET enable_team_account_billing = TRUE; 4 | -------------------------------------------------------------------------------- /backend/supabase/migrations/20250504123828_fix_thread_select_policy.sql: -------------------------------------------------------------------------------- 1 | DROP POLICY IF EXISTS thread_select_policy ON threads; 2 | 3 | CREATE POLICY thread_select_policy ON threads 4 | FOR SELECT 5 | USING ( 6 | is_public IS TRUE 7 | OR basejump.has_role_on_account(account_id) = true 8 | OR EXISTS ( 9 | SELECT 1 FROM projects 10 | WHERE projects.project_id = threads.project_id 11 | AND ( 12 | projects.is_public IS TRUE 13 | OR basejump.has_role_on_account(projects.account_id) = true 14 | ) 15 | ) 16 | ); 17 | -------------------------------------------------------------------------------- /backend/supabase/migrations/20250523133848_admin-view-access.sql: -------------------------------------------------------------------------------- 1 | DROP POLICY IF EXISTS "Give read only access to internal users" ON threads; 2 | 3 | CREATE POLICY "Give read only access to internal users" ON threads 4 | FOR SELECT 5 | USING ( 6 | ((auth.jwt() ->> 'email'::text) ~~ '%@kortix.ai'::text) 7 | ); 8 | 9 | 10 | DROP POLICY IF EXISTS "Give read only access to internal users" ON messages; 11 | 12 | CREATE POLICY "Give read only access to internal users" ON messages 13 | FOR SELECT 14 | USING ( 15 | ((auth.jwt() ->> 'email'::text) ~~ '%@kortix.ai'::text) 16 | ); 17 | 18 | 19 | DROP POLICY IF EXISTS "Give read only access to internal users" ON projects; 20 | 21 | CREATE POLICY "Give read only access to internal users" ON projects 22 | FOR SELECT 23 | USING ( 24 | ((auth.jwt() ->> 'email'::text) ~~ '%@kortix.ai'::text) 25 | ); 26 | -------------------------------------------------------------------------------- /backend/supabase/migrations/20250601000000_add_thread_metadata.sql: -------------------------------------------------------------------------------- 1 | -- Add metadata column to threads table to store additional context 2 | ALTER TABLE threads ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; 3 | 4 | -- Create index for metadata queries 5 | CREATE INDEX IF NOT EXISTS idx_threads_metadata ON threads USING GIN (metadata); 6 | 7 | -- Comment on the column 8 | COMMENT ON COLUMN threads.metadata IS 'Stores additional thread context like agent builder mode and target agent'; -------------------------------------------------------------------------------- /backend/supabase/migrations/20250602000000_add_custom_mcps_column.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE agents ADD COLUMN IF NOT EXISTS custom_mcps JSONB DEFAULT '[]'::jsonb; 4 | 5 | CREATE INDEX IF NOT EXISTS idx_agents_custom_mcps ON agents USING GIN (custom_mcps); 6 | 7 | COMMENT ON COLUMN agents.custom_mcps IS 'Stores custom MCP server configurations added by users (JSON or SSE endpoints)'; 8 | 9 | COMMIT; -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility functions and constants for agent tools -------------------------------------------------------------------------------- /backend/utils/s3_upload_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for handling image operations. 3 | """ 4 | 5 | import base64 6 | import uuid 7 | from datetime import datetime 8 | from utils.logger import logger 9 | from services.supabase import DBConnection 10 | 11 | async def upload_base64_image(base64_data: str, bucket_name: str = "browser-screenshots") -> str: 12 | """Upload a base64 encoded image to Supabase storage and return the URL. 13 | 14 | Args: 15 | base64_data (str): Base64 encoded image data (with or without data URL prefix) 16 | bucket_name (str): Name of the storage bucket to upload to 17 | 18 | Returns: 19 | str: Public URL of the uploaded image 20 | """ 21 | try: 22 | # Remove data URL prefix if present 23 | if base64_data.startswith('data:'): 24 | base64_data = base64_data.split(',')[1] 25 | 26 | # Decode base64 data 27 | image_data = base64.b64decode(base64_data) 28 | 29 | # Generate unique filename 30 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 31 | unique_id = str(uuid.uuid4())[:8] 32 | filename = f"image_{timestamp}_{unique_id}.png" 33 | 34 | # Upload to Supabase storage 35 | db = DBConnection() 36 | client = await db.client 37 | storage_response = await client.storage.from_(bucket_name).upload( 38 | filename, 39 | image_data, 40 | {"content-type": "image/png"} 41 | ) 42 | 43 | # Get public URL 44 | public_url = await client.storage.from_(bucket_name).get_public_url(filename) 45 | 46 | logger.debug(f"Successfully uploaded image to {public_url}") 47 | return public_url 48 | 49 | except Exception as e: 50 | logger.error(f"Error uploading base64 image: {e}") 51 | raise RuntimeError(f"Failed to upload image: {str(e)}") -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/docs/.DS_Store -------------------------------------------------------------------------------- /docs/images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/docs/images/diagram.png -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV_MODE="LOCAL" #production, or staging 2 | NEXT_PUBLIC_SUPABASE_URL="" 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY="" 4 | NEXT_PUBLIC_BACKEND_URL="" 5 | NEXT_PUBLIC_URL="" 6 | NEXT_PUBLIC_GOOGLE_CLIENT_ID="" 7 | OPENAI_API_KEY="" -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .env 3 | .env.local 4 | frontend/.env 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | .yarn/install-state.gz 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | !.env.local.example 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # supabase for testing 42 | supabase/.branches 43 | supabase/.temp 44 | supabase/**/*.env 45 | **/.prompts/ 46 | **/__pycache__/ 47 | 48 | # Sentry Config File 49 | .env.sentry-build-plugin 50 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .wrangler/ 4 | .editorconfig 5 | .eslintignore 6 | .gitignore 7 | .tool-versions 8 | .env 9 | .dev.vars 10 | pnpm-lock.yaml 11 | .next 12 | cache 13 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files first for better layer caching 6 | COPY package*.json ./ 7 | 8 | # Install build dependencies for node-gyp 9 | RUN apt-get update && apt-get install -y --no-install-recommends \ 10 | python3 \ 11 | make \ 12 | g++ \ 13 | build-essential \ 14 | pkg-config \ 15 | libcairo2-dev \ 16 | libpango1.0-dev \ 17 | libjpeg-dev \ 18 | libgif-dev \ 19 | librsvg2-dev \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | RUN npm install 23 | 24 | # Copy the frontend code 25 | COPY . . 26 | 27 | ENV NEXT_PUBLIC_VERCEL_ENV production 28 | 29 | RUN npm run build 30 | 31 | EXPOSE 3000 32 | 33 | # Default command is dev, but can be overridden in docker-compose 34 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Suna frontend 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | ``` 10 | 11 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 12 | 13 | ## Learn More 14 | 15 | To learn more about Next.js, take a look at the following resources: 16 | 17 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 18 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 19 | 20 | ## Deploy on Vercel 21 | 22 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 23 | 24 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 25 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.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 | -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-unused-vars': 'off', 17 | '@typescript-eslint/no-explicit-any': 'off', 18 | 'react/no-unescaped-entities': 'off', 19 | 'react-hooks/exhaustive-deps': 'warn', 20 | '@next/next/no-img-element': 'warn', 21 | '@typescript-eslint/no-empty-object-type': 'off', 22 | 'prefer-const': 'warn', 23 | }, 24 | }, 25 | ]; 26 | 27 | export default eslintConfig; 28 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import { withSentryConfig } from '@sentry/nextjs'; 2 | import type { NextConfig } from 'next'; 3 | 4 | let nextConfig: NextConfig = { 5 | webpack: (config) => { 6 | // This rule prevents issues with pdf.js and canvas 7 | config.externals = [...(config.externals || []), { canvas: 'canvas' }]; 8 | 9 | // Ensure node native modules are ignored 10 | config.resolve.fallback = { 11 | ...config.resolve.fallback, 12 | canvas: false, 13 | }; 14 | 15 | return config; 16 | }, 17 | }; 18 | 19 | if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'production') { 20 | nextConfig = withSentryConfig(nextConfig, { 21 | org: 'kortix-ai', 22 | project: 'suna-nextjs', 23 | silent: !process.env.CI, 24 | widenClientFileUpload: true, 25 | tunnelRoute: '/monitoring', 26 | disableLogger: true, 27 | automaticVercelMonitors: true, 28 | }); 29 | } 30 | 31 | export default nextConfig; 32 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/banner.png -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/holo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/holo.png -------------------------------------------------------------------------------- /frontend/public/mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/mac.png -------------------------------------------------------------------------------- /frontend/public/share-page/og-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/share-page/og-fallback.png -------------------------------------------------------------------------------- /frontend/public/thumbnail-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/thumbnail-dark.png -------------------------------------------------------------------------------- /frontend/public/thumbnail-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/thumbnail-light.png -------------------------------------------------------------------------------- /frontend/public/worldoscollage.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/public/worldoscollage.mp4 -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(personalAccount)/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // This component will be shown while the route is loading 4 | export default function Loading() { 5 | return ( 6 |
7 |
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(personalAccount)/settings/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/lib/supabase/server'; 2 | import AccountBillingStatus from '@/components/billing/account-billing-status'; 3 | 4 | const returnUrl = process.env.NEXT_PUBLIC_URL as string; 5 | 6 | export default async function PersonalAccountBillingPage() { 7 | const supabaseClient = await createClient(); 8 | const { data: personalAccount } = await supabaseClient.rpc( 9 | 'get_personal_account', 10 | ); 11 | 12 | return ( 13 |
14 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Separator } from '@/components/ui/separator'; 4 | import Link from 'next/link'; 5 | import { usePathname } from 'next/navigation'; 6 | 7 | export default function PersonalAccountSettingsPage({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const pathname = usePathname(); 13 | const items = [ 14 | // { name: "Profile", href: "/settings" }, 15 | // { name: "Teams", href: "/settings/teams" }, 16 | { name: 'Billing', href: '/settings/billing' }, 17 | ]; 18 | return ( 19 |
20 | 21 |
22 | 39 |
40 | {children} 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(personalAccount)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import EditPersonalAccountName from '@/components/basejump/edit-personal-account-name'; 2 | import { createClient } from '@/lib/supabase/server'; 3 | 4 | export default async function PersonalAccountSettingsPage() { 5 | const supabaseClient = await createClient(); 6 | const { data: personalAccount } = await supabaseClient.rpc( 7 | 'get_personal_account', 8 | ); 9 | 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(personalAccount)/settings/teams/page.tsx: -------------------------------------------------------------------------------- 1 | import ManageTeams from '@/components/basejump/manage-teams'; 2 | 3 | export default async function PersonalAccountTeamsPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(teamAccount)/[accountSlug]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | import React from 'react'; 5 | 6 | type AccountParams = { 7 | accountSlug: string; 8 | }; 9 | 10 | export default function AccountRedirect({ 11 | params, 12 | }: { 13 | params: Promise; 14 | }) { 15 | const unwrappedParams = React.use(params); 16 | const { accountSlug } = unwrappedParams; 17 | 18 | // Redirect to the settings page 19 | redirect(`/${accountSlug}/settings`); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/(teamAccount)/[accountSlug]/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Separator } from '@/components/ui/separator'; 5 | import Link from 'next/link'; 6 | import { usePathname } from 'next/navigation'; 7 | 8 | type LayoutParams = { 9 | accountSlug: string; 10 | }; 11 | 12 | export default function TeamSettingsLayout({ 13 | children, 14 | params, 15 | }: { 16 | children: React.ReactNode; 17 | params: Promise; 18 | }) { 19 | const unwrappedParams = React.use(params); 20 | const { accountSlug } = unwrappedParams; 21 | const pathname = usePathname(); 22 | const items = [ 23 | { name: 'Account', href: `/${accountSlug}/settings` }, 24 | { name: 'Members', href: `/${accountSlug}/settings/members` }, 25 | { name: 'Billing', href: `/${accountSlug}/settings/billing` }, 26 | ]; 27 | return ( 28 |
29 | 30 |
31 | 48 |
49 | {children} 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/[threadId]/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ThreadLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return <>{children}; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/[threadId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { 5 | ThreadParams, 6 | } from '@/components/thread/types'; 7 | import { RedirectPage } from './redirect-page'; 8 | 9 | export default function ThreadPage({ 10 | params, 11 | }: { 12 | params: Promise; 13 | }) { 14 | const unwrappedParams = React.use(params); 15 | return ; 16 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/[threadId]/redirect-page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useThreadQuery } from '@/hooks/react-query/threads/use-threads'; 6 | import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton'; 7 | 8 | interface RedirectPageProps { 9 | threadId: string; 10 | } 11 | 12 | export function RedirectPage({ threadId }: RedirectPageProps) { 13 | const router = useRouter(); 14 | const threadQuery = useThreadQuery(threadId); 15 | 16 | useEffect(() => { 17 | if (threadQuery.data?.project_id) { 18 | router.replace(`/projects/${threadQuery.data.project_id}/thread/${threadId}`); 19 | } 20 | }, [threadQuery.data, threadId, router]); 21 | 22 | if (threadQuery.isError) { 23 | router.replace('/dashboard'); 24 | return null; 25 | } 26 | return ; 27 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Bot, Search, Plus } from 'lucide-react'; 3 | import { Button } from '@/components/ui/button'; 4 | 5 | interface EmptyStateProps { 6 | hasAgents: boolean; 7 | onCreateAgent: () => void; 8 | onClearFilters: () => void; 9 | } 10 | 11 | export const EmptyState = ({ hasAgents, onCreateAgent, onClearFilters }: EmptyStateProps) => { 12 | return ( 13 |
14 |
15 |
16 | {!hasAgents ? ( 17 | 18 | ) : ( 19 | 20 | )} 21 |
22 |
23 |

24 | {!hasAgents ? 'No agents yet' : 'No agents found'} 25 |

26 |

27 | {!hasAgents ? ( 28 | 'Create your first agent to start automating tasks with custom instructions and tools. Configure custom AgentPress capabilities to fine tune agent according to your needs.' 29 | ) : ( 30 | 'No agents match your current search and filter criteria. Try adjusting your filters or search terms.' 31 | )} 32 |

33 |
34 | {!hasAgents ? ( 35 | 43 | ) : ( 44 | 51 | )} 52 |
53 |
54 | ); 55 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/loading-state.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 3 | import { Skeleton } from '@/components/ui/skeleton'; 4 | 5 | interface LoadingStateProps { 6 | viewMode: 'grid' | 'list'; 7 | } 8 | 9 | export const LoadingState = ({ viewMode }: LoadingStateProps) => { 10 | const skeletonCount = viewMode === 'grid' ? 4 : 8; 11 | 12 | return ( 13 |
14 | {Array.from({ length: skeletonCount }, (_, i) => ( 15 |
16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 |
25 | ))} 26 |
27 | ); 28 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/_loaders/mcp-search-loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | import { Card } from '@/components/ui/card'; 4 | 5 | export const McpSearchLoader: React.FC = () => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 | {Array.from({ length: 8 }).map((_, index) => ( 15 | 16 |
17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 | ))} 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/configured-mcp-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@/components/ui/card'; 3 | import { Button } from '@/components/ui/button'; 4 | import { Settings, X, Sparkles } from 'lucide-react'; 5 | import { MCPConfiguration } from './types'; 6 | 7 | 8 | interface ConfiguredMcpListProps { 9 | configuredMCPs: MCPConfiguration[]; 10 | onEdit: (index: number) => void; 11 | onRemove: (index: number) => void; 12 | } 13 | 14 | export const ConfiguredMcpList: React.FC = ({ 15 | configuredMCPs, 16 | onEdit, 17 | onRemove, 18 | }) => { 19 | if (configuredMCPs.length === 0) return null; 20 | 21 | return ( 22 |
23 | {configuredMCPs.map((mcp, index) => ( 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
{mcp.name}
32 |
33 | {mcp.enabledTools?.length || 0} tools enabled 34 |
35 |
36 |
37 |
38 | 45 | 52 |
53 |
54 |
55 | ))} 56 |
57 | ); 58 | }; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/constants.ts: -------------------------------------------------------------------------------- 1 | export const categoryIcons = { 2 | "AI & Search": "🤖", 3 | "Development & Version Control": "🔧", 4 | "Automation & Productivity": "⚡", 5 | "Communication & Collaboration": "💬", 6 | "Project Management": "📅", 7 | "Data & Analytics": "📊", 8 | "Cloud & Infrastructure": "☁️", 9 | "File Storage": "📁", 10 | "Marketing & Sales": "🛒", 11 | "Customer Support": "🎧", 12 | "Finance": "💰", 13 | "Utilities": "🔨", 14 | "Other": "🧩", 15 | 16 | "development": "🔧", 17 | "ai": "🤖", 18 | "automation": "⚡", 19 | "search": "🔍", 20 | "Database": "📊", 21 | "Web": "🌐", 22 | "File": "📄", 23 | "Development": "💻", 24 | "AI": "🤖", 25 | "Cloud": "☁️", 26 | "Utility": "⚡", 27 | "Integration": "🧩", 28 | }; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/mcp-server-card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@/components/ui/card'; 3 | import { Badge } from '@/components/ui/badge'; 4 | import { Shield, ExternalLink, ChevronRight, Sparkles } from 'lucide-react'; 5 | 6 | interface McpServerCardProps { 7 | server: any; 8 | onClick: (server: any) => void; 9 | } 10 | 11 | export const McpServerCard: React.FC = ({ server, onClick }) => { 12 | return ( 13 | onClick(server)} 16 | > 17 |
18 | {server.iconUrl ? ( 19 | {server.displayName 20 | ) : ( 21 |
22 | 23 |
24 | )} 25 |
26 |
27 |

{server.displayName || server.name}

28 | {server.security?.scanPassed && ( 29 | 30 | )} 31 | {server.isDeployed && ( 32 | 33 | Deployed 34 | 35 | )} 36 |
37 |

38 | {server.description} 39 |

40 |
41 | Used {server.useCount} times 42 | {server.homepage && ( 43 | 44 | )} 45 |
46 |
47 | 48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/search-results.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { McpSearchLoader } from './_loaders/mcp-search-loader'; 3 | import { McpServerCard } from './mcp-server-card'; 4 | 5 | interface SearchResultsProps { 6 | searchResults: any; 7 | isSearching: boolean; 8 | onServerSelect: (server: any) => void; 9 | } 10 | 11 | export const SearchResults: React.FC = ({ 12 | searchResults, 13 | isSearching, 14 | onServerSelect, 15 | }) => { 16 | if (isSearching) { 17 | return ; 18 | } 19 | 20 | if (!searchResults?.servers || searchResults.servers.length === 0) { 21 | return ( 22 |
23 |

No servers found

24 |
25 | ); 26 | } 27 | 28 | return ( 29 |
30 |

31 | Search Results ({searchResults.pagination.totalCount}) 32 |

33 |
34 | {searchResults.servers.map((server) => ( 35 | 40 | ))} 41 |
42 |
43 | ); 44 | }; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/mcp/types.ts: -------------------------------------------------------------------------------- 1 | export interface MCPConfiguration { 2 | name: string; 3 | qualifiedName: string; 4 | config: Record; 5 | enabledTools?: string[]; 6 | isCustom?: boolean; 7 | customType?: 'http' | 'sse'; 8 | } 9 | 10 | export interface MCPConfigurationProps { 11 | configuredMCPs: MCPConfiguration[]; 12 | onConfigurationChange: (mcps: MCPConfiguration[]) => void; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_components/results-info.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@/components/ui/button'; 3 | 4 | interface ResultsInfoProps { 5 | isLoading: boolean; 6 | totalAgents: number; 7 | filteredCount: number; 8 | searchQuery: string; 9 | activeFiltersCount: number; 10 | clearFilters: () => void; 11 | currentPage?: number; 12 | totalPages?: number; 13 | } 14 | 15 | export const ResultsInfo = ({ 16 | isLoading, 17 | totalAgents, 18 | filteredCount, 19 | searchQuery, 20 | activeFiltersCount, 21 | clearFilters, 22 | currentPage, 23 | totalPages 24 | }: ResultsInfoProps) => { 25 | if (isLoading || totalAgents === 0) { 26 | return null; 27 | } 28 | 29 | const showingText = () => { 30 | if (currentPage && totalPages && totalPages > 1) { 31 | return `Showing page ${currentPage} of ${totalPages} (${totalAgents} total agents)`; 32 | } 33 | return `Showing ${filteredCount} of ${totalAgents} agents`; 34 | }; 35 | 36 | return ( 37 |
38 | 39 | {showingText()} 40 | {searchQuery && ` for "${searchQuery}"`} 41 | 42 | {activeFiltersCount > 0 && ( 43 | 46 | )} 47 |
48 | ); 49 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_data/tools.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AGENTPRESS_TOOLS: Record = { 2 | 'sb_shell_tool': { enabled: false, description: 'Execute shell commands in tmux sessions for terminal operations, CLI tools, and system management', icon: '💻', color: 'bg-slate-100 dark:bg-slate-800' }, 3 | 'sb_files_tool': { enabled: false, description: 'Create, read, update, and delete files in the workspace with comprehensive file management', icon: '📁', color: 'bg-blue-100 dark:bg-blue-800/50' }, 4 | 'sb_browser_tool': { enabled: false, description: 'Browser automation for web navigation, clicking, form filling, and page interaction', icon: '🌐', color: 'bg-indigo-100 dark:bg-indigo-800/50' }, 5 | 'sb_deploy_tool': { enabled: false, description: 'Deploy applications and services with automated deployment capabilities', icon: '🚀', color: 'bg-green-100 dark:bg-green-800/50' }, 6 | 'sb_expose_tool': { enabled: false, description: 'Expose services and manage ports for application accessibility', icon: '🔌', color: 'bg-orange-100 dark:bg-orange-800/20' }, 7 | 'web_search_tool': { enabled: false, description: 'Search the web using Tavily API and scrape webpages with Firecrawl for research', icon: '🔍', color: 'bg-yellow-100 dark:bg-yellow-800/50' }, 8 | 'sb_vision_tool': { enabled: false, description: 'Vision and image processing capabilities for visual content analysis', icon: '👁️', color: 'bg-pink-100 dark:bg-pink-800/50' }, 9 | 'data_providers_tool': { enabled: false, description: 'Access to data providers and external APIs (requires RapidAPI key)', icon: '🔗', color: 'bg-cyan-100 dark:bg-cyan-800/50' }, 10 | }; 11 | 12 | export const getToolDisplayName = (toolName: string): string => { 13 | const displayNames: Record = { 14 | 'sb_shell_tool': 'Terminal', 15 | 'sb_files_tool': 'File Manager', 16 | 'sb_browser_tool': 'Browser Automation', 17 | 'sb_deploy_tool': 'Deploy Tool', 18 | 'sb_expose_tool': 'Port Exposure', 19 | 'web_search_tool': 'Web Search', 20 | 'sb_vision_tool': 'Image Processing', 21 | 'data_providers_tool': 'Data Providers', 22 | }; 23 | 24 | return displayNames[toolName] || toolName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); 25 | }; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_types/index.ts: -------------------------------------------------------------------------------- 1 | export type SortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count'; 2 | export type SortOrder = 'asc' | 'desc'; 3 | export type ViewMode = 'grid' | 'list'; 4 | 5 | export interface FilterOptions { 6 | hasDefaultAgent: boolean; 7 | hasMcpTools: boolean; 8 | hasAgentpressTools: boolean; 9 | selectedTools: string[]; 10 | } 11 | 12 | export interface Agent { 13 | agent_id: string; 14 | name: string; 15 | description?: string; 16 | is_default: boolean; 17 | created_at: string; 18 | updated_at?: string; 19 | configured_mcps?: Array<{ name: string }>; 20 | agentpress_tools?: Record; 21 | } 22 | 23 | export interface MutationState { 24 | isPending: boolean; 25 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/_utils/get-agent-style.ts: -------------------------------------------------------------------------------- 1 | export const getAgentAvatar = (agentId: string) => { 2 | const avatars = ['🤖', '🎯', '⚡', '🚀', '🔮', '🎨', '📊', '🔧', '💡', '🌟']; 3 | const colors = ['#06b6d4', '#22c55e', '#8b5cf6', '#3b82f6', '#ec4899', '#eab308', '#ef4444', '#6366f1']; 4 | const avatarIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % avatars.length; 5 | const colorIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length; 6 | return { 7 | avatar: avatars[avatarIndex], 8 | color: colors[colorIndex] 9 | }; 10 | }; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Agent Conversation | Kortix Suna', 5 | description: 'Interactive agent conversation powered by Kortix Suna', 6 | openGraph: { 7 | title: 'Agent Conversation | Kortix Suna', 8 | description: 'Interactive agent conversation powered by Kortix Suna', 9 | type: 'website', 10 | }, 11 | }; 12 | 13 | export default function AgentsLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return <>{children}; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/agents/new/[agentId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Create Agent | Kortix Suna', 5 | description: 'Interactive agent playground powered by Kortix Suna', 6 | openGraph: { 7 | title: 'Agent Playground | Kortix Suna', 8 | description: 'Interactive agent playground powered by Kortix Suna', 9 | type: 'website', 10 | }, 11 | }; 12 | 13 | export default function NewAgentLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return <>{children}; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/dashboard/_components/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/src/app/(dashboard)/dashboard/_components/index.ts -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/marketplace/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Agent Marketplace | Kortix Suna', 5 | description: 'Discover and add powerful AI agents created by the community to your personal library', 6 | openGraph: { 7 | title: 'Agent Marketplace | Kortix Suna', 8 | description: 'Discover and add powerful AI agents created by the community to your personal library', 9 | type: 'website', 10 | }, 11 | }; 12 | 13 | export default function MarketplaceLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return <>{children}; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ThreadLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return <>{children}; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/ThreadError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AlertTriangle } from 'lucide-react'; 3 | 4 | interface ThreadErrorProps { 5 | error: string; 6 | } 7 | 8 | export function ThreadError({ error }: ThreadErrorProps) { 9 | return ( 10 |
11 |
12 |
13 | 14 |
15 |

16 | Thread Not Found 17 |

18 |

19 | {error.includes( 20 | 'JSON object requested, multiple (or no) rows returned', 21 | ) 22 | ? 'This thread either does not exist or you do not have access to it.' 23 | : error 24 | } 25 |

26 |
27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/index.ts: -------------------------------------------------------------------------------- 1 | export { ThreadError } from './ThreadError'; 2 | export { UpgradeDialog } from './UpgradeDialog'; 3 | export { ThreadLayout } from './ThreadLayout'; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useThreadData } from './useThreadData'; 2 | export { useToolCalls } from './useToolCalls'; 3 | export { useBilling } from './useBilling'; 4 | export { useKeyboardShortcuts } from './useKeyboardShortcuts'; -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/useKeyboardShortcuts.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | interface UseKeyboardShortcutsProps { 4 | isSidePanelOpen: boolean; 5 | setIsSidePanelOpen: (open: boolean) => void; 6 | leftSidebarState: string; 7 | setLeftSidebarOpen: (open: boolean) => void; 8 | userClosedPanelRef: React.MutableRefObject; 9 | } 10 | 11 | export function useKeyboardShortcuts({ 12 | isSidePanelOpen, 13 | setIsSidePanelOpen, 14 | leftSidebarState, 15 | setLeftSidebarOpen, 16 | userClosedPanelRef, 17 | }: UseKeyboardShortcutsProps) { 18 | useEffect(() => { 19 | const handleKeyDown = (event: KeyboardEvent) => { 20 | if ((event.metaKey || event.ctrlKey) && event.key === 'i') { 21 | event.preventDefault(); 22 | if (isSidePanelOpen) { 23 | setIsSidePanelOpen(false); 24 | userClosedPanelRef.current = true; 25 | } else { 26 | setIsSidePanelOpen(true); 27 | setLeftSidebarOpen(false); 28 | } 29 | } 30 | 31 | if ((event.metaKey || event.ctrlKey) && event.key === 'b') { 32 | event.preventDefault(); 33 | if (leftSidebarState === 'expanded') { 34 | setLeftSidebarOpen(false); 35 | } else { 36 | setLeftSidebarOpen(true); 37 | if (isSidePanelOpen) { 38 | setIsSidePanelOpen(false); 39 | userClosedPanelRef.current = true; 40 | } 41 | } 42 | } 43 | 44 | if (event.key === 'Escape' && isSidePanelOpen) { 45 | setIsSidePanelOpen(false); 46 | userClosedPanelRef.current = true; 47 | } 48 | }; 49 | 50 | window.addEventListener('keydown', handleKeyDown); 51 | return () => window.removeEventListener('keydown', handleKeyDown); 52 | }, [isSidePanelOpen, leftSidebarState, setLeftSidebarOpen, setIsSidePanelOpen, userClosedPanelRef]); 53 | } -------------------------------------------------------------------------------- /frontend/src/app/(dashboard)/projects/[projectId]/thread/_types/index.ts: -------------------------------------------------------------------------------- 1 | import { Message as BaseApiMessageType } from '@/lib/api'; 2 | 3 | // Re-export types from the shared thread types (except ApiMessageType which we're extending) 4 | export type { 5 | UnifiedMessage, 6 | ParsedMetadata, 7 | ThreadParams, 8 | ParsedContent, 9 | } from '@/components/thread/types'; 10 | 11 | // Re-export other needed types 12 | export type { ToolCallInput } from '@/components/thread/tool-call-side-panel'; 13 | export type { Project } from '@/lib/api'; 14 | 15 | // Local types specific to this page 16 | export interface ApiMessageType extends BaseApiMessageType { 17 | message_id?: string; 18 | thread_id?: string; 19 | is_llm_message?: boolean; 20 | metadata?: string; 21 | created_at?: string; 22 | updated_at?: string; 23 | } 24 | 25 | export interface StreamingToolCall { 26 | id?: string; 27 | name?: string; 28 | arguments?: string; 29 | index?: number; 30 | xml_tag_name?: string; 31 | } 32 | 33 | export interface BillingData { 34 | currentUsage?: number; 35 | limit?: number; 36 | message?: string; 37 | accountId?: string | null; 38 | } 39 | 40 | export type AgentStatus = 'idle' | 'running' | 'connecting' | 'error'; -------------------------------------------------------------------------------- /frontend/src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from '@/components/home/sections/navbar'; 2 | 3 | export default function HomeLayout({ 4 | children, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | }>) { 8 | return ( 9 |
10 |
11 |
12 | 13 | {children} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { CTASection } from '@/components/home/sections/cta-section'; 5 | // import { FAQSection } from "@/components/sections/faq-section"; 6 | import { FooterSection } from '@/components/home/sections/footer-section'; 7 | import { HeroSection } from '@/components/home/sections/hero-section'; 8 | import { OpenSourceSection } from '@/components/home/sections/open-source-section'; 9 | import { PricingSection } from '@/components/home/sections/pricing-section'; 10 | import { UseCasesSection } from '@/components/home/sections/use-cases-section'; 11 | import { ModalProviders } from '@/providers/modal-providers'; 12 | 13 | export default function Home() { 14 | return ( 15 | <> 16 | 17 |
18 |
19 | 20 | 21 | {/* */} 22 | {/* */} 23 | {/* */} 24 | {/* */} 25 | {/* */} 26 | 27 | 28 | {/* */} 29 | {/* */} 30 | 31 | 32 |
33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/api/share-page/og-image/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | // Add route segment config for caching 4 | export const runtime = 'edge'; // Use edge runtime for better performance 5 | export const revalidate = 3600; // Cache for 1 hour 6 | 7 | export async function GET(request) { 8 | const { searchParams } = new URL(request.url); 9 | const title = searchParams.get('title'); 10 | 11 | // Add error handling 12 | if (!title) { 13 | return new NextResponse('Missing title parameter', { status: 400 }); 14 | } 15 | 16 | try { 17 | const response = await fetch(`https://api.orshot.com/v1/studio/render`, { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | Authorization: `Bearer ${process.env.ORSHOT_API_KEY}`, 22 | }, 23 | body: JSON.stringify({ 24 | templateId: 10, 25 | modifications: { 26 | title, 27 | }, 28 | response: { 29 | type: 'binary', 30 | }, 31 | }), 32 | }); 33 | 34 | if (!response.ok) { 35 | throw new Error(`Orshot API error: ${response.status}`); 36 | } 37 | 38 | const blob = await response.blob(); 39 | const arrayBuffer = await blob.arrayBuffer(); 40 | const image = Buffer.from(arrayBuffer); 41 | 42 | return new NextResponse(image, { 43 | status: 200, 44 | headers: { 45 | 'Content-Type': 'image/png', 46 | 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', 47 | }, 48 | }); 49 | } catch (error) { 50 | console.error('OG Image generation error:', error); 51 | return new NextResponse('Error generating image', { status: 500 }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/lib/supabase/server'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function GET(request: Request) { 5 | // The `/auth/callback` route is required for the server-side auth flow implemented 6 | // by the SSR package. It exchanges an auth code for the user's session. 7 | // https://supabase.com/docs/guides/auth/server-side/nextjs 8 | const requestUrl = new URL(request.url); 9 | const code = requestUrl.searchParams.get('code'); 10 | const returnUrl = requestUrl.searchParams.get('returnUrl'); 11 | const origin = requestUrl.origin; 12 | 13 | if (code) { 14 | const supabase = await createClient(); 15 | await supabase.auth.exchangeCodeForSession(code); 16 | } 17 | 18 | // URL to redirect to after sign up process completes 19 | // Handle the case where returnUrl is 'null' (string) or actual null 20 | const redirectPath = 21 | returnUrl && returnUrl !== 'null' ? returnUrl : '/dashboard'; 22 | // Make sure to include a slash between origin and path if needed 23 | return NextResponse.redirect( 24 | `${origin}${redirectPath.startsWith('/') ? '' : '/'}${redirectPath}`, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortix-ai/suna/1bdd3fd0b110a79cd809e29f79db360e4c558037/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as Sentry from '@sentry/nextjs'; 4 | import NextError from 'next/error'; 5 | import { useEffect } from 'react'; 6 | 7 | export default function GlobalError({ 8 | error, 9 | }: { 10 | error: Error & { digest?: string }; 11 | }) { 12 | useEffect(() => { 13 | Sentry.captureException(error); 14 | }, [error]); 15 | 16 | return ( 17 | 18 | 19 | {/* `NextError` is the default Next.js error page component. Its type 20 | definition requires a `statusCode` prop. However, since the App Router 21 | does not expose status codes for errors, we simply pass 0 to render a 22 | generic error message. */} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/invitation/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import AcceptTeamInvitation from '@/components/basejump/accept-team-invitation'; 5 | import { redirect } from 'next/navigation'; 6 | 7 | type InvitationSearchParams = { 8 | token?: string; 9 | }; 10 | 11 | export default function AcceptInvitationPage({ 12 | searchParams, 13 | }: { 14 | searchParams: Promise; 15 | }) { 16 | const unwrappedSearchParams = React.use(searchParams); 17 | 18 | if (!unwrappedSearchParams.token) { 19 | redirect('/'); 20 | } 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { siteConfig } from '@/lib/site'; 3 | 4 | export const metadata: Metadata = { 5 | title: siteConfig.name, 6 | description: siteConfig.description, 7 | keywords: ['Kortix Suna', 'AI', 'Agent'], 8 | authors: [ 9 | { 10 | name: 'Kortix AI Corp', 11 | url: 'https://kortix.ai', 12 | }, 13 | ], 14 | creator: 'Kortix AI Corp', 15 | openGraph: { 16 | type: 'website', 17 | locale: 'en_US', 18 | url: siteConfig.url, 19 | title: siteConfig.name, 20 | description: siteConfig.description, 21 | siteName: siteConfig.name, 22 | }, 23 | twitter: { 24 | card: 'summary_large_image', 25 | title: siteConfig.name, 26 | description: siteConfig.description, 27 | creator: '@kortixai', 28 | }, 29 | robots: { 30 | index: true, 31 | follow: true, 32 | googleBot: { 33 | index: true, 34 | follow: true, 35 | 'max-video-preview': -1, 36 | 'max-image-preview': 'large', 37 | 'max-snippet': -1, 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/app/monitoring/route.ts: -------------------------------------------------------------------------------- 1 | const SENTRY_URL = new URL( 2 | process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://example.com/abc', 3 | ); 4 | const SENTRY_HOST = SENTRY_URL.hostname; 5 | const SENTRY_PROJECT_ID = SENTRY_URL.pathname.split('/').pop(); 6 | 7 | export const POST = async (req: Request) => { 8 | try { 9 | if (!process.env.NEXT_PUBLIC_SENTRY_DSN) { 10 | return Response.json( 11 | { error: 'Sentry is not configured' }, 12 | { status: 500 }, 13 | ); 14 | } 15 | 16 | const envelopeBytes = await req.arrayBuffer(); 17 | const envelope = new TextDecoder().decode(envelopeBytes); 18 | const piece = envelope.split('\n')[0]; 19 | const header = JSON.parse(piece) as { dsn: string }; 20 | const dsn = new URL(header.dsn); 21 | const project_id = dsn.pathname.replace('/', ''); 22 | 23 | if (dsn.hostname !== SENTRY_HOST) { 24 | throw new Error(`Invalid sentry hostname: ${dsn.hostname}`); 25 | } 26 | 27 | if (project_id !== SENTRY_PROJECT_ID) { 28 | throw new Error(`Invalid sentry project id: ${project_id}`); 29 | } 30 | 31 | const upstream_sentry_url = `https://${SENTRY_HOST}/api/${project_id}/envelope/`; 32 | const response = await fetch(upstream_sentry_url, { 33 | body: envelopeBytes, 34 | method: 'POST', 35 | }); 36 | 37 | return response; 38 | } catch (e) { 39 | console.error('error tunneling to sentry', e); 40 | return Response.json( 41 | { error: 'error tunneling to sentry' }, 42 | { status: 500 }, 43 | ); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/app/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import { ImageResponse } from 'next/og'; 3 | 4 | // Configuration exports 5 | export const runtime = 'edge'; 6 | export const alt = 'Kortix Suna'; 7 | export const size = { 8 | width: 1200, 9 | height: 630, 10 | }; 11 | export const contentType = 'image/png'; 12 | 13 | export default async function Image() { 14 | try { 15 | // Get the host from headers 16 | const headersList = await headers(); 17 | const host = headersList.get('host') || ''; 18 | const protocol = process.env.NODE_ENV === 'development' ? 'http' : 'https'; 19 | const baseUrl = `${protocol}://${host}`; 20 | 21 | return new ImageResponse( 22 | ( 23 |
33 | {alt} 42 |
43 | ), 44 | { ...size }, 45 | ); 46 | } catch (error) { 47 | console.error('Error generating OpenGraph image:', error); 48 | return new Response(`Failed to generate image`, { status: 500 }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider } from 'next-themes'; 4 | import { useState, createContext, useEffect } from 'react'; 5 | import { AuthProvider } from '@/components/AuthProvider'; 6 | import { ReactQueryProvider } from '@/providers/react-query-provider'; 7 | import { dehydrate, QueryClient } from '@tanstack/react-query'; 8 | 9 | export interface ParsedTag { 10 | tagName: string; 11 | attributes: Record; 12 | content: string; 13 | isClosing: boolean; 14 | id: string; // Unique ID for each tool call instance 15 | rawMatch?: string; // Raw XML match for deduplication 16 | timestamp?: number; // Timestamp when the tag was created 17 | 18 | // Pairing and completion status 19 | resultTag?: ParsedTag; // Reference to the result tag if this is a tool call 20 | isToolCall?: boolean; // Whether this is a tool call (vs a result) 21 | isPaired?: boolean; // Whether this tag has been paired with its call/result 22 | status?: 'running' | 'completed' | 'error'; // Status of the tool call 23 | 24 | // VNC preview for browser-related tools 25 | vncPreview?: string; // VNC preview image URL 26 | } 27 | 28 | // Create the context here instead of importing it 29 | export const ToolCallsContext = createContext<{ 30 | toolCalls: ParsedTag[]; 31 | setToolCalls: React.Dispatch>; 32 | }>({ 33 | toolCalls: [], 34 | setToolCalls: () => { }, 35 | }); 36 | 37 | export function Providers({ children }: { children: React.ReactNode }) { 38 | // Shared state for tool calls across the app 39 | const [toolCalls, setToolCalls] = useState([]); 40 | const queryClient = new QueryClient(); 41 | const dehydratedState = dehydrate(queryClient); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | {children} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/share/[threadId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { getThread, getProject } from '@/lib/api-server'; 3 | 4 | export async function generateMetadata({ params }): Promise { 5 | const { threadId } = await params; 6 | const fallbackMetaData = { 7 | title: 'Shared Conversation | Kortix Suna', 8 | description: 'Replay this Agent conversation on Kortix Suna', 9 | alternates: { 10 | canonical: `${process.env.NEXT_PUBLIC_URL}/share/${threadId}`, 11 | }, 12 | openGraph: { 13 | title: 'Shared Conversation | Kortix Suna', 14 | description: 'Replay this Agent conversation on Kortix Suna', 15 | images: [`${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`], 16 | }, 17 | }; 18 | 19 | try { 20 | const threadData = await getThread(threadId); 21 | const projectData = await getProject(threadData.project_id); 22 | 23 | if (!threadData || !projectData) { 24 | return fallbackMetaData; 25 | } 26 | 27 | const isDevelopment = 28 | process.env.NODE_ENV === 'development' || 29 | process.env.NEXT_PUBLIC_ENV_MODE === 'LOCAL' || 30 | process.env.NEXT_PUBLIC_ENV_MODE === 'local'; 31 | 32 | const title = projectData.name || 'Shared Conversation | Kortix Suna'; 33 | const description = 34 | projectData.description || 35 | 'Replay this Agent conversation on Kortix Suna'; 36 | const ogImage = isDevelopment 37 | ? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png` 38 | : `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`; 39 | 40 | return { 41 | title, 42 | description, 43 | alternates: { 44 | canonical: `${process.env.NEXT_PUBLIC_URL}/share/${threadId}`, 45 | }, 46 | openGraph: { 47 | title, 48 | description, 49 | images: [ogImage], 50 | }, 51 | twitter: { 52 | title, 53 | description, 54 | images: ogImage, 55 | card: 'summary_large_image', 56 | }, 57 | }; 58 | } catch (error) { 59 | return fallbackMetaData; 60 | } 61 | } 62 | 63 | export default async function ThreadLayout({ children }) { 64 | return <>{children}; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/accept-team-invitation.tsx: -------------------------------------------------------------------------------- 1 | import { acceptInvitation } from '@/lib/actions/invitations'; 2 | import { createClient } from '@/lib/supabase/server'; 3 | import { Alert } from '../ui/alert'; 4 | import { Card, CardContent } from '../ui/card'; 5 | import { SubmitButton } from '../ui/submit-button'; 6 | 7 | type Props = { 8 | token: string; 9 | }; 10 | export default async function AcceptTeamInvitation({ token }: Props) { 11 | const supabaseClient = await createClient(); 12 | const { data: invitation } = await supabaseClient.rpc('lookup_invitation', { 13 | lookup_invitation_token: token, 14 | }); 15 | 16 | return ( 17 | 18 | 19 |
20 |

You've been invited to join

21 |

{invitation.account_name}

22 |
23 | {Boolean(invitation.active) ? ( 24 |
25 | 26 | 30 | Accept invitation 31 | 32 |
33 | ) : ( 34 | 35 | This invitation has been deactivated. Please contact the account 36 | owner for a new invitation. 37 | 38 | )} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/create-team-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | } from '@/components/ui/dialog'; 11 | import NewTeamForm from '@/components/basejump/new-team-form'; 12 | 13 | interface CreateTeamDialogProps { 14 | open: boolean; 15 | onOpenChange: (open: boolean) => void; 16 | } 17 | export function CreateTeamDialog({ 18 | open, 19 | onOpenChange, 20 | }: CreateTeamDialogProps) { 21 | return ( 22 | 23 | 24 | 25 | 26 | Create a new team 27 | 28 | 29 | Create a team to collaborate with others. 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/create-team-invitation-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from '@/components/ui/dialog'; 12 | import NewInvitationForm from './new-invitation-form'; 13 | 14 | type Props = { 15 | accountId: string; 16 | }; 17 | 18 | export default function CreateTeamInvitationButton({ accountId }: Props) { 19 | return ( 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | Invite Team Member 33 | 34 | 35 | Send an email invitation to join your team 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/delete-team-member-form.tsx: -------------------------------------------------------------------------------- 1 | import { SubmitButton } from '../ui/submit-button'; 2 | import { removeTeamMember } from '@/lib/actions/members'; 3 | import { GetAccountMembersResponse } from '@usebasejump/shared'; 4 | import { usePathname } from 'next/navigation'; 5 | 6 | type Props = { 7 | accountId: string; 8 | teamMember: GetAccountMembersResponse[0]; 9 | }; 10 | 11 | export default function DeleteTeamMemberForm({ accountId, teamMember }: Props) { 12 | const pathName = usePathname(); 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 25 | Remove member 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/edit-personal-account-name.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@/components/ui/input'; 2 | import { SubmitButton } from '../ui/submit-button'; 3 | import { Label } from '../ui/label'; 4 | import { GetAccountResponse } from '@usebasejump/shared'; 5 | import { editPersonalAccountName } from '@/lib/actions/personal-account'; 6 | 7 | type Props = { 8 | account: GetAccountResponse; 9 | }; 10 | 11 | export default function EditPersonalAccountName({ account }: Props) { 12 | return ( 13 |
14 | 15 |
16 |
17 | 23 | 31 |
32 |
33 | 38 | Save Changes 39 | 40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/edit-team-name.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@/components/ui/input'; 2 | import { SubmitButton } from '../ui/submit-button'; 3 | import { editTeamName } from '@/lib/actions/teams'; 4 | import { Label } from '../ui/label'; 5 | import { GetAccountResponse } from '@usebasejump/shared'; 6 | 7 | type Props = { 8 | account: GetAccountResponse; 9 | }; 10 | 11 | export default function EditTeamName({ account }: Props) { 12 | return ( 13 |
14 | 15 |
16 |
17 | 23 | 31 |
32 |
33 | 38 | Save Changes 39 | 40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/basejump/edit-team-slug.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input } from '@/components/ui/input'; 4 | import { SubmitButton } from '../ui/submit-button'; 5 | import { editTeamSlug } from '@/lib/actions/teams'; 6 | import { Label } from '../ui/label'; 7 | import { GetAccountResponse } from '@usebasejump/shared'; 8 | 9 | type Props = { 10 | account: GetAccountResponse; 11 | }; 12 | 13 | export default function EditTeamSlug({ account }: Props) { 14 | return ( 15 |
16 | 17 |
18 |
19 | 25 |
26 | 27 | https://your-app.com/ 28 | 29 | 37 |
38 |
39 |
40 | 45 | Save Changes 46 | 47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/components/examples/ErrorHandlingDemo.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/home/second-bento-animation.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from '@/components/home/icons'; 2 | import { OrbitingCircles } from '@/components/home/ui/orbiting-circle'; 3 | 4 | export function SecondBentoAnimation() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/components/home/section-header.tsx: -------------------------------------------------------------------------------- 1 | interface SectionHeaderProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export function SectionHeader({ children }: SectionHeaderProps) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/bento-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SectionHeader } from '@/components/home/section-header'; 4 | import { siteConfig } from '@/lib/home'; 5 | 6 | export function BentoSection() { 7 | const { title, description, items } = siteConfig.bentoSection; 8 | 9 | return ( 10 |
14 |
15 | 16 |

17 | {title} 18 |

19 |

20 | {description} 21 |

22 |
23 | 24 |
25 | {items.map((item) => ( 26 |
30 |
31 | {item.content} 32 |
33 |
34 |

35 | {item.title} 36 |

37 |

{item.description}

38 |
39 |
40 | ))} 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/company-showcase.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from '@/lib/home'; 2 | import { ArrowRight } from 'lucide-react'; 3 | import Link from 'next/link'; 4 | 5 | export function CompanyShowcase() { 6 | const { companyShowcase } = siteConfig; 7 | return ( 8 |
12 |

13 | Trusted by fast-growing startups 14 |

15 |
16 | {companyShowcase.companyLogos.map((logo) => ( 17 | 22 |
23 | {logo.logo} 24 |
25 |
26 | 27 | Learn More 28 | 29 |
30 | 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/cta-section.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { siteConfig } from '@/lib/home'; 3 | import Link from 'next/link'; 4 | 5 | export function CTASection() { 6 | const { ctaSection } = siteConfig; 7 | 8 | return ( 9 |
13 |
14 |
15 | {/* Agent CTA Background */} 22 |
23 |

24 | {ctaSection.title} 25 |

26 |
27 | 31 | {ctaSection.button.text} 32 | 33 | {ctaSection.subtext} 34 |
35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/faq-section.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | AccordionContent, 4 | AccordionItem, 5 | AccordionTrigger, 6 | } from '@/components/home/ui/accordion'; 7 | import { SectionHeader } from '@/components/home/section-header'; 8 | import { siteConfig } from '@/lib/home'; 9 | 10 | export function FAQSection() { 11 | const { faqSection } = siteConfig; 12 | 13 | return ( 14 |
18 | 19 |

20 | {faqSection.title} 21 |

22 |

23 | {faqSection.description} 24 |

25 |
26 | 27 |
28 | 33 | {faqSection.faQitems.map((faq, index) => ( 34 | 39 | 40 | {faq.question} 41 | 42 | 43 |

44 | {faq.answer} 45 |

46 |
47 |
48 | ))} 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/feature-section.tsx: -------------------------------------------------------------------------------- 1 | import { SectionHeader } from '@/components/home/section-header'; 2 | import { Feature as FeatureComponent } from '@/components/home/ui/feature-slideshow'; 3 | import { siteConfig } from '@/lib/home'; 4 | 5 | export function FeatureSection() { 6 | const { title, description, items } = siteConfig.featureSection; 7 | 8 | return ( 9 |
13 | 14 |

15 | {title} 16 |

17 |

18 | {description} 19 |

20 |
21 |
22 | 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/growth-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SectionHeader } from '@/components/home/section-header'; 4 | import { siteConfig } from '@/lib/home'; 5 | 6 | export function GrowthSection() { 7 | const { title, description, items } = siteConfig.growthSection; 8 | 9 | return ( 10 |
14 |
15 | {/* Decorative borders */} 16 |
17 |
18 | 19 | {/* Section Header */} 20 | 21 |

22 | {title} 23 |

24 |

25 | {description} 26 |

27 |
28 | 29 | {/* Grid Layout */} 30 |
31 | {items.map((item) => ( 32 |
36 | {item.content} 37 |

38 | {item.title} 39 |

40 |

{item.description}

41 |
42 | ))} 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/hero-video-section.tsx: -------------------------------------------------------------------------------- 1 | import { HeroVideoDialog } from '@/components/home/ui/hero-video-dialog'; 2 | 3 | export function HeroVideoSection() { 4 | return ( 5 |
6 |
7 | 14 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/quote-section.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { siteConfig } from '@/lib/home'; 3 | 4 | export function QuoteSection() { 5 | const { quoteSection } = siteConfig; 6 | 7 | return ( 8 |
12 |
13 |

14 | {quoteSection.quote} 15 |

16 | 17 |
18 |
19 | {quoteSection.author.name} 24 |
25 |
26 | 27 | {quoteSection.author.name} 28 | 29 |

{quoteSection.author.role}

30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/home/sections/testimonial-section.tsx: -------------------------------------------------------------------------------- 1 | import { SectionHeader } from '@/components/home/section-header'; 2 | import { SocialProofTestimonials } from '@/components/home/testimonial-scroll'; 3 | import { siteConfig } from '@/lib/home'; 4 | 5 | export function TestimonialSection() { 6 | const { testimonials } = siteConfig; 7 | 8 | return ( 9 |
13 | 14 |

15 | Empower Your Workflow with AI 16 |

17 |

18 | Ask your AI Agent for real-time collaboration, seamless integrations, 19 | and actionable insights to streamline your operations. 20 |

21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/home/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/home/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { Moon, Sun } from 'lucide-react'; 5 | import { useTheme } from 'next-themes'; 6 | import { Button } from '@/components/home/ui/button'; 7 | 8 | export function ThemeToggle() { 9 | const { theme, setTheme } = useTheme(); 10 | 11 | return ( 12 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/home/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', 14 | destructive: 15 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', 16 | outline: 17 | 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', 18 | secondary: 19 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', 20 | ghost: 'hover:bg-accent hover:text-accent-foreground', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | }, 23 | size: { 24 | default: 'h-9 px-4 py-2 has-[>svg]:px-3', 25 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', 26 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', 27 | icon: 'size-9', 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: 'default', 32 | size: 'default', 33 | }, 34 | }, 35 | ); 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<'button'> & 44 | VariantProps & { 45 | asChild?: boolean; 46 | }) { 47 | const Comp = asChild ? Slot : 'button'; 48 | 49 | return ( 50 | 55 | ); 56 | } 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /frontend/src/components/home/ui/marquee.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentPropsWithoutRef } from 'react'; 3 | 4 | interface MarqueeProps extends ComponentPropsWithoutRef<'div'> { 5 | /** 6 | * Optional CSS class name to apply custom styles 7 | */ 8 | className?: string; 9 | /** 10 | * Whether to reverse the animation direction 11 | * @default false 12 | */ 13 | reverse?: boolean; 14 | /** 15 | * Whether to pause the animation on hover 16 | * @default false 17 | */ 18 | pauseOnHover?: boolean; 19 | /** 20 | * Content to be displayed in the marquee 21 | */ 22 | children: React.ReactNode; 23 | /** 24 | * Whether to animate vertically instead of horizontally 25 | * @default false 26 | */ 27 | vertical?: boolean; 28 | /** 29 | * Number of times to repeat the content 30 | * @default 4 31 | */ 32 | repeat?: number; 33 | } 34 | 35 | export function Marquee({ 36 | className, 37 | reverse = false, 38 | pauseOnHover = false, 39 | children, 40 | vertical = false, 41 | repeat = 4, 42 | ...props 43 | }: MarqueeProps) { 44 | return ( 45 |
56 | {Array(repeat) 57 | .fill(0) 58 | .map((_, i) => ( 59 |
68 | {children} 69 |
70 | ))} 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/components/sentry/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import * as Sentry from '@sentry/nextjs'; 5 | import { useAuth } from '../AuthProvider'; 6 | 7 | export const VSentry: React.FC = () => { 8 | const { user } = useAuth(); 9 | useEffect(() => { 10 | if (!document) return; 11 | const scope = Sentry.getCurrentScope(); 12 | if (!user) scope.setUser(null); 13 | else scope.setUser({ email: user.email, id: user.id }); 14 | }, [user]); 15 | 16 | return null; 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/cta.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import Link from 'next/link'; 3 | import { Briefcase, ExternalLink } from 'lucide-react'; 4 | import { KortixProcessModal } from '@/components/sidebar/kortix-enterprise-modal'; 5 | 6 | export function CTACard() { 7 | return ( 8 |
9 |
10 |
11 | 12 | Enterprise Demo 13 | 14 | 15 | AI employees for your company 16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | 30 | 31 | Join Our Team! 🚀 32 | 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/date-picker.tsx: -------------------------------------------------------------------------------- 1 | import { Calendar } from '@/components/ui/calendar'; 2 | import { SidebarGroup, SidebarGroupContent } from '@/components/ui/sidebar'; 3 | 4 | export function DatePicker() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/kortix-logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import { useTheme } from 'next-themes'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | interface KortixLogoProps { 8 | size?: number; 9 | } 10 | export function KortixLogo({ size = 24 }: KortixLogoProps) { 11 | const { theme } = useTheme(); 12 | const [mounted, setMounted] = useState(false); 13 | 14 | // After mount, we can access the theme 15 | useEffect(() => { 16 | setMounted(true); 17 | }, []); 18 | 19 | return ( 20 | Kortix 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/nav-main.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type LucideIcon } from 'lucide-react'; 4 | 5 | import { 6 | SidebarMenu, 7 | SidebarMenuButton, 8 | SidebarMenuItem, 9 | } from '@/components/ui/sidebar'; 10 | 11 | export function NavMain({ 12 | items, 13 | }: { 14 | items: { 15 | title: string; 16 | url: string; 17 | icon: LucideIcon; 18 | isActive?: boolean; 19 | }[]; 20 | }) { 21 | return ( 22 | 23 | {items.map((item) => ( 24 | 25 | 26 | 27 | 28 | {item.title} 29 | 30 | 31 | 32 | ))} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | ThemeProvider as NextThemesProvider, 6 | type ThemeProviderProps, 7 | } from 'next-themes'; 8 | 9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/thread/preview-renderers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './html-renderer'; 2 | export * from './markdown-renderer'; 3 | export * from './csv-renderer'; -------------------------------------------------------------------------------- /frontend/src/components/thread/preview-renderers/markdown-renderer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { ScrollArea } from '@/components/ui/scroll-area'; 5 | import { Markdown } from '@/components/ui/markdown'; 6 | import { cn } from '@/lib/utils'; 7 | 8 | interface MarkdownRendererProps { 9 | content: string; 10 | className?: string; 11 | } 12 | 13 | /** 14 | * Renderer for Markdown content with scrollable container 15 | */ 16 | export function MarkdownRenderer({ 17 | content, 18 | className 19 | }: MarkdownRendererProps) { 20 | return ( 21 |
22 | 23 |
24 | 27 | {content} 28 | 29 |
30 |
31 |
32 | ); 33 | } -------------------------------------------------------------------------------- /frontend/src/components/thread/tool-views/shared/ImageLoader.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export const ImageLoader = ({ className = "" } : {className?: string}) => { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /frontend/src/components/thread/tool-views/types.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '@/lib/api'; 2 | 3 | export interface ToolViewProps { 4 | assistantContent?: string; 5 | toolContent?: string; 6 | assistantTimestamp?: string; 7 | toolTimestamp?: string; 8 | isSuccess?: boolean; 9 | isStreaming?: boolean; 10 | project?: Project; 11 | name?: string; 12 | messages?: any[]; 13 | agentStatus?: string; 14 | currentIndex?: number; 15 | totalCalls?: number; 16 | onFileClick?: (filePath: string) => void; 17 | } 18 | 19 | export interface BrowserToolViewProps extends ToolViewProps { 20 | name?: string; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/thread/tool-views/wrapper/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ToolViewWrapper'; 2 | export * from './ToolViewRegistry'; 3 | -------------------------------------------------------------------------------- /frontend/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 25 | ) 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 35 | svg]:rotate-180", 39 | className 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps) { 55 | return ( 56 | 61 |
{children}
62 |
63 | ) 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 67 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-card text-card-foreground', 12 | destructive: 13 | 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<'div'> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { 38 | return ( 39 |
47 | ); 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<'div'>) { 54 | return ( 55 |
63 | ); 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription }; 67 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const badgeVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', 14 | secondary: 15 | 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', 16 | destructive: 17 | 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 18 | outline: 19 | 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', 20 | new: 21 | 'text-purple-600 dark:text-purple-300 bg-purple-600/30 dark:bg-purple-600/30', 22 | beta: 23 | 'text-blue-600 dark:text-blue-300 bg-blue-600/30 dark:bg-blue-600/30', 24 | highlight: 25 | 'text-green-800 dark:text-green-300 bg-green-600/30 dark:bg-green-600/30', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | }, 31 | }, 32 | ); 33 | 34 | function Badge({ 35 | className, 36 | variant, 37 | asChild = false, 38 | ...props 39 | }: React.ComponentProps<'span'> & 40 | VariantProps & { asChild?: boolean }) { 41 | const Comp = asChild ? Slot : 'span'; 42 | 43 | return ( 44 | 49 | ); 50 | } 51 | 52 | export { Badge, badgeVariants }; 53 | -------------------------------------------------------------------------------- /frontend/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { CheckIcon } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | -------------------------------------------------------------------------------- /frontend/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 4 | 5 | function Collapsible({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return ; 9 | } 10 | 11 | function CollapsibleTrigger({ 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 19 | ); 20 | } 21 | 22 | function CollapsibleContent({ 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 30 | ); 31 | } 32 | 33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 34 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<'input'>) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Popover({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return ; 12 | } 13 | 14 | function PopoverTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return ; 18 | } 19 | 20 | function PopoverContent({ 21 | className, 22 | align = 'center', 23 | sideOffset = 4, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | 38 | 39 | ); 40 | } 41 | 42 | function PopoverAnchor({ 43 | ...props 44 | }: React.ComponentProps) { 45 | return ; 46 | } 47 | 48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 49 | -------------------------------------------------------------------------------- /frontend/src/components/ui/portal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | interface PortalProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export function Portal({ children }: PortalProps) { 9 | const [mounted, setMounted] = useState(false); 10 | 11 | useEffect(() => { 12 | setMounted(true); 13 | return () => setMounted(false); 14 | }, []); 15 | 16 | if (!mounted) return null; 17 | 18 | return createPortal(children, document.body); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Progress({ 9 | className, 10 | value, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 27 | 28 | ) 29 | } 30 | 31 | export { Progress } 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; 5 | import { CircleIcon } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | function RadioGroup({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | ); 20 | } 21 | 22 | function RadioGroupItem({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 35 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export { RadioGroup, RadioGroupItem }; 46 | -------------------------------------------------------------------------------- /frontend/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function ScrollArea({ 9 | className, 10 | children, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function ScrollBar({ 32 | className, 33 | orientation = 'vertical', 34 | ...props 35 | }: React.ComponentProps) { 36 | return ( 37 | 50 | 54 | 55 | ); 56 | } 57 | 58 | export { ScrollArea, ScrollBar }; 59 | -------------------------------------------------------------------------------- /frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | import React from 'react'; 5 | 6 | function Skeleton({ 7 | className, 8 | ...props 9 | }: React.HTMLAttributes) { 10 | return ( 11 |
20 |
21 |
22 |
23 | 71 |
72 | ); 73 | } 74 | 75 | export { Skeleton }; 76 | -------------------------------------------------------------------------------- /frontend/src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SliderPrimitive from '@radix-ui/react-slider'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Slider({ 9 | className, 10 | defaultValue, 11 | value, 12 | min = 0, 13 | max = 100, 14 | ...props 15 | }: React.ComponentProps) { 16 | const _values = React.useMemo( 17 | () => 18 | Array.isArray(value) 19 | ? value 20 | : Array.isArray(defaultValue) 21 | ? defaultValue 22 | : [min, max], 23 | [value, defaultValue, min, max], 24 | ); 25 | 26 | return ( 27 | 39 | 45 | 51 | 52 | {Array.from({ length: _values.length }, (_, index) => ( 53 | 58 | ))} 59 | 60 | ); 61 | } 62 | 63 | export { Slider }; 64 | -------------------------------------------------------------------------------- /frontend/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner, ToasterProps } from 'sonner'; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /frontend/src/components/ui/status-overlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; 3 | import { useDeleteOperation } from '@/contexts/DeleteOperationContext'; 4 | 5 | export function StatusOverlay() { 6 | const { state } = useDeleteOperation(); 7 | 8 | if (state.operation === 'none' || !state.isDeleting) return null; 9 | 10 | return ( 11 |
12 | {state.operation === 'pending' && ( 13 | <> 14 | 15 | Processing... 16 | 17 | )} 18 | 19 | {state.operation === 'success' && ( 20 | <> 21 | 22 | Completed 23 | 24 | )} 25 | 26 | {state.operation === 'error' && ( 27 | <> 28 | 29 | Failed 30 | 31 | )} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/ui/submit-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormStatus } from 'react-dom'; 4 | import { useActionState } from 'react'; 5 | import { type ComponentProps } from 'react'; 6 | import { Button } from '@/components/ui/button'; 7 | import { Alert, AlertDescription } from './alert'; 8 | import { AlertTriangle } from 'lucide-react'; 9 | 10 | type Props = Omit, 'formAction'> & { 11 | pendingText?: string; 12 | formAction: (prevState: any, formData: FormData) => Promise; 13 | errorMessage?: string; 14 | }; 15 | 16 | const initialState = { 17 | message: '', 18 | }; 19 | 20 | export function SubmitButton({ 21 | children, 22 | formAction, 23 | errorMessage, 24 | pendingText = 'Submitting...', 25 | ...props 26 | }: Props) { 27 | const { pending, action } = useFormStatus(); 28 | const [state, internalFormAction] = useActionState(formAction, initialState); 29 | 30 | const isPending = pending && action === internalFormAction; 31 | 32 | return ( 33 |
34 | {Boolean(errorMessage || state?.message) && ( 35 | 36 | 37 | {errorMessage || state?.message} 38 | 39 | )} 40 |
41 | 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitive from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ) 29 | } 30 | 31 | export { Switch } 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function TabsList({ 22 | className, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 34 | ); 35 | } 36 | 37 | function TabsTrigger({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | function TabsContent({ 54 | className, 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 63 | ); 64 | } 65 | 66 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 67 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |