From 9872d17abe9db7d5af58d99ceaccb058cf4317b3 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Sun, 20 Oct 2024 12:06:46 +0200 Subject: [PATCH] feat: AI-based Open-Text Summary & new Experience page (#3038) Co-authored-by: Johannes Co-authored-by: Piyush Gupta Co-authored-by: pandeymangg --- .devcontainer/docker-compose.yml | 11 +- .env.example | 6 + .github/workflows/e2e.yml | 3 +- LICENSE | 2 +- .../components/OnboardingOptionsContainer.tsx | 7 +- .../environments/[environmentId]/layout.tsx | 4 +- .../edit/components/EditEndingCard.tsx | 9 +- .../edit/components/EditWelcomeCard.tsx | 4 +- .../edit/components/HiddenFieldsCard.tsx | 6 +- .../edit/components/QuestionCard.tsx | 5 +- .../edit/components/QuestionsDroppable.tsx | 6 +- .../edit/components/QuestionsView.tsx | 5 +- .../edit/components/SurveyVariablesCard.tsx | 6 +- .../surveys/[surveyId]/edit/lib/utils.tsx | 9 +- .../surveys/templates/actions.ts | 79 + .../templates/components/FormbricksAICard.tsx | 83 + .../components/TemplateContainer.tsx | 13 + .../surveys/templates/page.tsx | 2 + .../components/EnvironmentLayout.tsx | 4 + .../components/MainNavigation.tsx | 13 +- .../[environmentId]/experience/page.tsx | 3 + .../components/AddIntegrationModal.tsx | 4 +- .../[environmentId]/integrations/page.tsx | 2 +- .../components/AddChannelMappingModal.tsx | 4 +- .../environments/[environmentId]/layout.tsx | 2 +- .../environments/[environmentId]/page.tsx | 16 +- .../notifications/components/EditAlerts.tsx | 2 +- .../components/EditWeeklySummary.tsx | 2 +- .../components/OrganizationSettingsNavbar.tsx | 8 +- .../{members => general}/actions.ts | 19 + .../general/components/AIToggle.tsx | 82 + .../components/AddMemberModal.tsx | 0 .../components/BulkInviteTab.tsx | 0 .../components/DeleteOrganization.tsx | 2 +- .../EditMemberships/EditMemberships.tsx | 2 +- .../EditMemberships/MemberActions.tsx | 4 +- .../EditMemberships/MembersInfo.tsx | 2 +- .../EditMemberships/OrganizationActions.tsx | 4 +- .../components/EditMemberships/index.ts | 0 .../components/EditOrganizationName.tsx | 2 +- .../components/EditOrganizationNameForm.tsx | 2 +- .../components/IndividualInviteTab.tsx | 0 .../components/ShareInviteModal.tsx | 0 .../{members => general}/loading.tsx | 2 +- .../{members => general}/page.tsx | 19 +- .../surveys/[surveyId]/(analysis)/actions.ts | 20 +- .../responses/components/ResponseTable.tsx | 2 +- .../[surveyId]/(analysis)/responses/page.tsx | 20 +- .../summary/components/CTASummary.tsx | 2 +- .../summary/components/ConsentSummary.tsx | 3 +- .../components/EnableInsightsBanner.tsx | 56 + .../components/MatrixQuestionSummary.tsx | 3 +- .../components/MultipleChoiceSummary.tsx | 3 +- .../summary/components/NPSSummary.tsx | 3 +- .../summary/components/OpenTextSummary.tsx | 149 +- .../components/PictureChoiceSummary.tsx | 3 +- .../components/QuestionSummaryHeader.tsx | 6 +- .../summary/components/RatingSummary.tsx | 5 +- .../summary/components/ScrollToTop.tsx | 2 +- .../summary/components/SummaryList.tsx | 10 +- .../summary/components/SummaryPage.tsx | 6 + .../(analysis)/summary/lib/insights.ts | 74 + .../(analysis)/summary/lib/surveySummary.ts | 916 +++++++++++ .../(analysis)/summary/lib/utils.ts | 17 +- .../[surveyId]/(analysis)/summary/page.tsx | 23 +- .../lib/checkoutSessionCompleted.ts | 0 .../billing/stripe-webhook/lib/constants.ts | 0 .../lib/createCustomerPortalSession.ts | 0 .../stripe-webhook/lib/createSubscription.ts | 0 .../stripe-webhook/lib/invoiceFinalized.ts | 0 .../lib/isSubscriptionCancelled.ts | 0 .../stripe-webhook/lib/stripeWebhook.ts | 0 .../lib/subscriptionCreatedOrUpdated.ts | 0 .../stripe-webhook/lib/subscriptionDeleted.ts | 0 .../api/billing/stripe-webhook/route.ts | 0 .../environments/[environmentId]/layout.tsx | 3 + .../(organization)/billing/actions.ts | 6 +- .../billing/components/PricingTable.tsx | 4 +- .../(organization)/billing/layout.tsx | 0 .../(organization)/billing/loading.tsx | 0 .../settings/(organization)/billing/page.tsx | 0 .../environments/[environmentId]/layout.tsx | 67 - .../csv-conversion/route.ts | 0 .../excel-conversion/route.ts | 0 .../api/(internal)/insights/lib/document.ts | 79 + .../api/(internal)/insights/lib/insights.ts | 418 +++++ .../app/api/(internal)/insights/lib/utils.ts | 135 ++ apps/web/app/api/(internal)/insights/route.ts | 53 + .../api/(internal)/pipeline/lib/documents.ts | 107 ++ .../pipeline/lib/handleIntegrations.ts | 0 .../api/{ => (internal)}/pipeline/route.ts | 41 +- apps/web/app/lib/fetchFile.ts | 2 +- apps/web/app/lib/utils.ts | 20 + .../[sharingKey]/(analysis)/summary/page.tsx | 1 + apps/web/app/share/[sharingKey]/actions.ts | 2 +- apps/web/instrumentation.ts | 16 + apps/web/lib/cache/document.ts | 66 + apps/web/lib/cache/insight.ts | 25 + .../components/insight-sheet/actions.ts | 96 ++ .../components/insight-sheet/index.tsx | 170 ++ .../components/insight-sheet/lib/documents.ts | 126 ++ .../ee/insights/components/insights-view.tsx | 171 ++ .../modules/ee/insights/experience/actions.ts | 51 + .../experience/components/dashboard.tsx | 71 + .../experience/components/greeting.tsx | 24 + .../experience/components/insight-loading.tsx | 28 + .../experience/components/insight-view.tsx | 197 +++ .../experience/components/insights-card.tsx | 37 + .../insights/experience/components/stats.tsx | 104 ++ .../experience/components/templates-card.tsx | 37 + .../ee/insights/experience/lib/insights.ts | 84 + .../ee/insights/experience/lib/stats.ts | 104 ++ .../ee/insights/experience/lib/utils.ts | 18 + .../modules/ee/insights/experience/page.tsx | 59 + .../ee/insights/experience/types/stats.ts | 14 + apps/web/next.config.mjs | 1 + apps/web/package.json | 5 + apps/web/playwright/organization.spec.ts | 6 +- apps/web/playwright/survey.spec.ts | 3 +- docker-compose.yml | 3 +- docker/docker-compose.yml | 2 +- packages/config-typescript/base.json | 3 +- .../data-migration.ts | 8 +- packages/database/docker-compose.yml | 7 +- .../migration.sql | 77 + packages/database/schema.prisma | 70 +- .../components/multi-language-card.tsx | 6 +- .../components/secondary-language-select.tsx | 4 +- packages/ee/package.json | 2 + packages/lib/{.eslintrc.js => .eslintrc.cjs} | 0 packages/lib/aiModels.ts | 14 + packages/lib/constants.ts | 12 + packages/lib/env.ts | 18 + packages/lib/organization/service.ts | 11 +- packages/lib/package.json | 1 + packages/lib/response/service.ts | 65 +- .../lib/response/tests/__mocks__/data.mock.ts | 2 + packages/lib/response/tests/response.test.ts | 2 +- packages/lib/response/utils.ts | 834 +--------- packages/lib/survey/service.ts | 112 +- packages/lib/survey/tests/survey.test.ts | 2 + packages/lib/survey/utils.ts | 27 +- packages/lib/utils/ai.ts | 15 + packages/lib/vite.config.ts | 2 + .../src/components/general/Headline.tsx | 4 +- .../src/components/general/HtmlBody.tsx | 3 +- .../src/components/general/ProgressBar.tsx | 4 +- .../general/QuestionConditional.tsx | 3 +- .../src/components/general/Subheader.tsx | 4 +- .../surveys/src/components/general/Survey.tsx | 13 +- .../components/questions/AddressQuestion.tsx | 4 +- .../src/components/questions/CTAQuestion.tsx | 4 +- .../src/components/questions/CalQuestion.tsx | 4 +- .../components/questions/ConsentQuestion.tsx | 4 +- .../questions/ContactInfoQuestion.tsx | 4 +- .../src/components/questions/DateQuestion.tsx | 4 +- .../questions/FileUploadQuestion.tsx | 4 +- .../components/questions/MatrixQuestion.tsx | 4 +- .../questions/MultipleChoiceMultiQuestion.tsx | 4 +- .../MultipleChoiceSingleQuestion.tsx | 4 +- .../src/components/questions/NPSQuestion.tsx | 4 +- .../components/questions/OpenTextQuestion.tsx | 4 +- .../questions/PictureSelectionQuestion.tsx | 4 +- .../components/questions/RankingQuestion.tsx | 8 +- .../components/questions/RatingQuestion.tsx | 4 +- .../wrappers/StackedCardsContainer.tsx | 6 +- packages/surveys/src/lib/ttc.ts | 5 +- packages/types/document-insights.ts | 9 + packages/types/documents.ts | 56 + packages/types/insights.ts | 43 + packages/types/organizations.ts | 2 + packages/types/surveys/types.ts | 7 +- packages/types/surveys/validation.ts | 3 +- packages/ui/components/Card/index.tsx | 113 +- packages/ui/components/Card/stories.tsx | 2 +- .../components/CardArrangementTabs/index.tsx | 4 +- .../ui/components/IntegrationCard/index.tsx | 65 + .../ui/components/IntegrationCard/stories.tsx | 69 + packages/ui/components/OptionCard/index.tsx | 6 +- .../ui/components/PreviewSurvey/index.tsx | 4 +- .../components/RecallItemSelect.tsx | 3 +- .../components/SecondaryNavigation/index.tsx | 91 +- packages/ui/components/Separator/index.tsx | 25 + packages/ui/components/Sheet/index.tsx | 119 ++ packages/ui/components/StylingTabs/index.tsx | 78 + packages/ui/components/Tabs/index.tsx | 120 +- .../components/StartFromScratchTemplate.tsx | 6 + .../TemplateList/components/Template.tsx | 6 + packages/ui/components/TemplateList/index.tsx | 10 +- packages/ui/components/Textarea/index.tsx | 20 + packages/ui/components/ToggleGroup/index.tsx | 52 + packages/ui/components/ToggleGroup/toggle.tsx | 39 + packages/ui/components/Typography/index.tsx | 155 ++ packages/ui/package.json | 4 + packages/ui/tailwind.config.js | 2 +- pnpm-lock.yaml | 1373 ++++++++++++----- turbo.json | 9 + 197 files changed, 6382 insertions(+), 1752 deletions(-) create mode 100644 apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts create mode 100644 apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/experience/page.tsx rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/actions.ts (91%) create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle.tsx rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/AddMemberModal.tsx (100%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/BulkInviteTab.tsx (100%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/DeleteOrganization.tsx (98%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditMemberships/EditMemberships.tsx (96%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditMemberships/MemberActions.tsx (98%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditMemberships/MembersInfo.tsx (98%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditMemberships/OrganizationActions.tsx (97%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditMemberships/index.ts (100%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditOrganizationName.tsx (98%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/EditOrganizationNameForm.tsx (98%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/IndividualInviteTab.tsx (100%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/components/ShareInviteModal.tsx (100%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/loading.tsx (97%) rename apps/web/app/(app)/environments/[environmentId]/settings/(organization)/{members => general}/page.tsx (86%) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/checkoutSessionCompleted.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/constants.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/createCustomerPortalSession.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/createSubscription.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/invoiceFinalized.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/isSubscriptionCancelled.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/stripeWebhook.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/subscriptionCreatedOrUpdated.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/lib/subscriptionDeleted.ts (100%) rename apps/web/app/(ee)/{ => (billing)}/api/billing/stripe-webhook/route.ts (100%) create mode 100644 apps/web/app/(ee)/(billing)/environments/[environmentId]/layout.tsx rename apps/web/app/(ee)/{ => (billing)}/environments/[environmentId]/settings/(organization)/billing/actions.ts (88%) rename apps/web/app/(ee)/{ => (billing)}/environments/[environmentId]/settings/(organization)/billing/components/PricingTable.tsx (97%) rename apps/web/app/(ee)/{ => (billing)}/environments/[environmentId]/settings/(organization)/billing/layout.tsx (100%) rename apps/web/app/(ee)/{ => (billing)}/environments/[environmentId]/settings/(organization)/billing/loading.tsx (100%) rename apps/web/app/(ee)/{ => (billing)}/environments/[environmentId]/settings/(organization)/billing/page.tsx (100%) delete mode 100644 apps/web/app/(ee)/environments/[environmentId]/layout.tsx rename apps/web/app/api/{internal => (internal)}/csv-conversion/route.ts (100%) rename apps/web/app/api/{internal => (internal)}/excel-conversion/route.ts (100%) create mode 100644 apps/web/app/api/(internal)/insights/lib/document.ts create mode 100644 apps/web/app/api/(internal)/insights/lib/insights.ts create mode 100644 apps/web/app/api/(internal)/insights/lib/utils.ts create mode 100644 apps/web/app/api/(internal)/insights/route.ts create mode 100644 apps/web/app/api/(internal)/pipeline/lib/documents.ts rename apps/web/app/api/{ => (internal)}/pipeline/lib/handleIntegrations.ts (100%) rename apps/web/app/api/{ => (internal)}/pipeline/route.ts (74%) create mode 100644 apps/web/lib/cache/document.ts create mode 100644 apps/web/lib/cache/insight.ts create mode 100644 apps/web/modules/ee/insights/components/insight-sheet/actions.ts create mode 100644 apps/web/modules/ee/insights/components/insight-sheet/index.tsx create mode 100644 apps/web/modules/ee/insights/components/insight-sheet/lib/documents.ts create mode 100644 apps/web/modules/ee/insights/components/insights-view.tsx create mode 100644 apps/web/modules/ee/insights/experience/actions.ts create mode 100644 apps/web/modules/ee/insights/experience/components/dashboard.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/greeting.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/insight-loading.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/insight-view.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/insights-card.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/stats.tsx create mode 100644 apps/web/modules/ee/insights/experience/components/templates-card.tsx create mode 100644 apps/web/modules/ee/insights/experience/lib/insights.ts create mode 100644 apps/web/modules/ee/insights/experience/lib/stats.ts create mode 100644 apps/web/modules/ee/insights/experience/lib/utils.ts create mode 100644 apps/web/modules/ee/insights/experience/page.tsx create mode 100644 apps/web/modules/ee/insights/experience/types/stats.ts create mode 100644 packages/database/migrations/20241017124431_add_documents_and_insights/migration.sql rename packages/lib/{.eslintrc.js => .eslintrc.cjs} (100%) create mode 100644 packages/lib/aiModels.ts create mode 100644 packages/lib/utils/ai.ts create mode 100644 packages/types/document-insights.ts create mode 100644 packages/types/documents.ts create mode 100644 packages/types/insights.ts create mode 100644 packages/ui/components/IntegrationCard/index.tsx create mode 100644 packages/ui/components/IntegrationCard/stories.tsx create mode 100644 packages/ui/components/Separator/index.tsx create mode 100644 packages/ui/components/Sheet/index.tsx create mode 100644 packages/ui/components/StylingTabs/index.tsx create mode 100644 packages/ui/components/Textarea/index.tsx create mode 100644 packages/ui/components/ToggleGroup/index.tsx create mode 100644 packages/ui/components/ToggleGroup/toggle.tsx create mode 100644 packages/ui/components/Typography/index.tsx diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index cc35a8d7fb..ec935b66ba 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: app: @@ -22,11 +22,11 @@ services: # Uncomment the next line to use a non-root user for all processes. # user: node - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. # (Adding the "ports" property to this file will not forward from a Codespace.) db: - image: postgres:latest + image: pgvector/pgvector:pg17 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data @@ -41,12 +41,11 @@ services: image: mailhog/mailhog network_mode: service:app logging: - driver: "none" # disable saving logs + driver: + "none" # disable saving logs # ports: # - 8025:8025 # web ui # 1025:1025 # smtp server - - volumes: postgres-data: null diff --git a/.env.example b/.env.example index ed19ac159d..bfa0f955c5 100644 --- a/.env.example +++ b/.env.example @@ -180,3 +180,9 @@ UNSPLASH_ACCESS_KEY= # Disable custom cache handler if necessary (e.g. if deployed on Vercel) # CUSTOM_CACHE_DISABLED=1 + +# Azure AI settings +# AI_AZURE_RESSOURCE_NAME= +# AI_AZURE_API_KEY= +# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID= +# AI_AZURE_LLM_DEPLOYMENT_ID= \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 445d9d909e..1f0bb5cfcc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 60 services: postgres: - image: postgres:latest + image: pgvector/pgvector:pg17 env: POSTGRES_DB: postgres POSTGRES_USER: postgres @@ -50,6 +50,7 @@ jobs: sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env + echo "" >> .env echo "E2E_TESTING=1" >> .env shell: bash diff --git a/LICENSE b/LICENSE index b0a7cb4564..24abe033c6 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Copyright (c) 2024 Formbricks GmbH Portions of this software are licensed as follows: -- All content that resides under the "packages/ee/" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE". +- All content that resides under the "packages/ee/", "apps/web/modules/ee" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE". - All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages. - All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component. - Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below. diff --git a/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.tsx b/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.tsx index 18d67cbf25..ce39289155 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.tsx @@ -1,7 +1,6 @@ import { LucideProps } from "lucide-react"; import Link from "next/link"; import { ForwardRefExoticComponent, RefAttributes } from "react"; -import { cn } from "@formbricks/lib/cn"; import { OptionCard } from "@formbricks/ui/components/OptionCard"; interface OnboardingOptionsContainerProps { @@ -40,11 +39,7 @@ export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContain }; return ( -
= 3, - "flex justify-center gap-8": options.length < 3, - })}> +
{options.map((option) => option.href ? ( diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx index 8a38c27267..f47b8f8c0c 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx @@ -12,7 +12,7 @@ import { AuthorizationError } from "@formbricks/types/errors"; import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner"; import { ToasterClient } from "@formbricks/ui/components/ToasterClient"; -const EnvLayout = async ({ children, params }) => { +const SurveyEditorEnvironmentLayout = async ({ children, params }) => { const session = await getServerSession(authOptions); if (!session || !session.user) { return redirect(`/auth/login`); @@ -61,4 +61,4 @@ const EnvLayout = async ({ children, params }) => { ); }; -export default EnvLayout; +export default SurveyEditorEnvironmentLayout; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx index 154ce13284..d795e0358a 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditEndingCard.tsx @@ -13,7 +13,12 @@ import { cn } from "@formbricks/lib/cn"; import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TOrganizationBillingPlan } from "@formbricks/types/organizations"; -import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types"; +import { + TSurvey, + TSurveyEndScreenCard, + TSurveyQuestionId, + TSurveyRedirectUrlCard, +} from "@formbricks/types/surveys/types"; import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch"; import { TooltipRenderer } from "@formbricks/ui/components/Tooltip"; @@ -22,7 +27,7 @@ interface EditEndingCardProps { endingCardIndex: number; setLocalSurvey: React.Dispatch>; setActiveQuestionId: (id: string | null) => void; - activeQuestionId: string | null; + activeQuestionId: TSurveyQuestionId | null; isInvalid: boolean; selectedLanguageCode: string; setSelectedLanguageCode: (languageCode: string) => void; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx index 8770214566..aa957f1851 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx @@ -7,7 +7,7 @@ import { useState } from "react"; import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor"; import { cn } from "@formbricks/lib/cn"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types"; import { FileInput } from "@formbricks/ui/components/FileInput"; import { Label } from "@formbricks/ui/components/Label"; import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput"; @@ -17,7 +17,7 @@ interface EditWelcomeCardProps { localSurvey: TSurvey; setLocalSurvey: (survey: TSurvey) => void; setActiveQuestionId: (id: string | null) => void; - activeQuestionId: string | null; + activeQuestionId: TSurveyQuestionId | null; isInvalid: boolean; selectedLanguageCode: string; setSelectedLanguageCode: (languageCode: string) => void; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx index 98d6b11c8a..d3f743a108 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx @@ -7,7 +7,7 @@ import { useState } from "react"; import { toast } from "react-hot-toast"; import { cn } from "@formbricks/lib/cn"; import { extractRecallInfo } from "@formbricks/lib/utils/recall"; -import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyHiddenFields, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { validateId } from "@formbricks/types/surveys/validation"; import { Button } from "@formbricks/ui/components/Button"; import { Input } from "@formbricks/ui/components/Input"; @@ -18,8 +18,8 @@ import { Tag } from "@formbricks/ui/components/Tag"; interface HiddenFieldsCardProps { localSurvey: TSurvey; setLocalSurvey: (survey: TSurvey) => void; - activeQuestionId: string | null; - setActiveQuestionId: (questionId: string | null) => void; + activeQuestionId: TSurveyQuestionId | null; + setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void; } export const HiddenFieldsCard = ({ diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx index 7e8d21a3ea..bb59974804 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx @@ -17,6 +17,7 @@ import { TI18nString, TSurvey, TSurveyQuestion, + TSurveyQuestionId, TSurveyQuestionTypeEnum, } from "@formbricks/types/surveys/types"; import { Label } from "@formbricks/ui/components/Label"; @@ -46,8 +47,8 @@ interface QuestionCardProps { updateQuestion: (questionIdx: number, updatedAttributes: any) => void; deleteQuestion: (questionIdx: number) => void; duplicateQuestion: (questionIdx: number) => void; - activeQuestionId: string | null; - setActiveQuestionId: (questionId: string | null) => void; + activeQuestionId: TSurveyQuestionId | null; + setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void; lastQuestion: boolean; selectedLanguageCode: string; setSelectedLanguageCode: (language: string) => void; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx index 234176dd69..c42817fc6b 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx @@ -1,7 +1,7 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TProduct } from "@formbricks/types/product"; -import { TSurvey } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { QuestionCard } from "./QuestionCard"; interface QuestionsDraggableProps { @@ -11,8 +11,8 @@ interface QuestionsDraggableProps { updateQuestion: (questionIdx: number, updatedAttributes: any) => void; deleteQuestion: (questionIdx: number) => void; duplicateQuestion: (questionIdx: number) => void; - activeQuestionId: string | null; - setActiveQuestionId: (questionId: string | null) => void; + activeQuestionId: TSurveyQuestionId | null; + setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void; selectedLanguageCode: string; setSelectedLanguageCode: (language: string) => void; invalidQuestions: string[] | null; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx index 5e3bdb3d7f..f402d813b1 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx @@ -29,6 +29,7 @@ import { TSingleCondition, TSurveyLogic, TSurveyLogicAction, + TSurveyQuestionId, } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types"; import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation"; @@ -47,8 +48,8 @@ import { QuestionsDroppable } from "./QuestionsDroppable"; interface QuestionsViewProps { localSurvey: TSurvey; setLocalSurvey: React.Dispatch>; - activeQuestionId: string | null; - setActiveQuestionId: (questionId: string | null) => void; + activeQuestionId: TSurveyQuestionId | null; + setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void; product: TProduct; invalidQuestions: string[] | null; setInvalidQuestions: React.Dispatch>; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx index 686e7b6af3..640969ac7e 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard.tsx @@ -3,14 +3,14 @@ import * as Collapsible from "@radix-ui/react-collapsible"; import { FileDigitIcon } from "lucide-react"; import { cn } from "@formbricks/lib/cn"; -import { TSurvey } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem"; interface SurveyVariablesCardProps { localSurvey: TSurvey; setLocalSurvey: (survey: TSurvey) => void; - activeQuestionId: string | null; - setActiveQuestionId: (id: string | null) => void; + activeQuestionId: TSurveyQuestionId | null; + setActiveQuestionId: (id: TSurveyQuestionId | null) => void; } const variablesCardId = `fb-variables-${Date.now()}`; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx index 12a0bbbc7b..afc12ef718 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx @@ -15,6 +15,7 @@ import { TSurveyLogicAction, TSurveyLogicConditionsOperator, TSurveyQuestion, + TSurveyQuestionId, TSurveyQuestionTypeEnum, TSurveyVariable, } from "@formbricks/types/surveys/types"; @@ -1023,7 +1024,7 @@ const isUsedInRightOperand = ( } }; -export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): number => { +export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQuestionId): number => { const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => { if (isConditionGroup(condition)) { // It's a TConditionGroup @@ -1053,7 +1054,11 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): nu ); }; -export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optionId: string): number => { +export const findOptionUsedInLogic = ( + survey: TSurvey, + questionId: TSurveyQuestionId, + optionId: string +): number => { const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => { if (isConditionGroup(condition)) { // It's a TConditionGroup diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts new file mode 100644 index 0000000000..2ddb015d13 --- /dev/null +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts @@ -0,0 +1,79 @@ +"use server"; + +import { createId } from "@paralleldrive/cuid2"; +import { generateObject } from "ai"; +import { z } from "zod"; +import { authenticatedActionClient } from "@formbricks/lib/actionClient"; +import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; +import { llmModel } from "@formbricks/lib/aiModels"; +import { getOrganization } from "@formbricks/lib/organization/service"; +import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils"; +import { createSurvey } from "@formbricks/lib/survey/service"; +import { getIsAIEnabled } from "@formbricks/lib/utils/ai"; +import { ZId, ZString } from "@formbricks/types/common"; +import { ZSurveyQuestion } from "@formbricks/types/surveys/types"; + +const ZCreateAISurveyAction = z.object({ + environmentId: ZId, + prompt: ZString, +}); + +export const createAISurveyAction = authenticatedActionClient + .schema(ZCreateAISurveyAction) + .action(async ({ ctx, parsedInput }) => { + const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId); + + await checkAuthorization({ + userId: ctx.user.id, + organizationId, + rules: ["survey", "create"], + }); + + const organization = await getOrganization(organizationId); + + if (!organization) { + throw new Error("Organization not found"); + } + + const isAIEnabled = await getIsAIEnabled(organization.billing.plan); + + if (!isAIEnabled) { + throw new Error("AI is not enabled for this organization"); + } + + const { object } = await generateObject({ + model: llmModel, + schema: z.object({ + name: z.string(), + questions: z.array( + z.object({ + headline: z.string(), + subheader: z.string(), + type: z.enum(["openText", "multipleChoiceSingle", "multipleChoiceMulti"]), + choices: z + .array(z.string()) + .min(2, { message: "Multiple Choice Question must have at least two choices" }) + .optional(), + }) + ), + }), + system: `You are a survey AI. Create a survey with 3 questions max that fits the schema and user input.`, + prompt: parsedInput.prompt, + experimental_telemetry: { isEnabled: true }, + }); + + const parsedQuestions = object.questions.map((question) => { + return ZSurveyQuestion.parse({ + id: createId(), + headline: { default: question.headline }, + subheader: { default: question.subheader }, + type: question.type, + choices: question.choices + ? question.choices.map((choice) => ({ id: createId(), label: { default: choice } })) + : undefined, + required: true, + }); + }); + + return await createSurvey(parsedInput.environmentId, { name: object.name, questions: parsedQuestions }); + }); diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx new file mode 100644 index 0000000000..3dd07eeafe --- /dev/null +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { createAISurveyAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions"; +import { Sparkles } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper"; +import { Button } from "@formbricks/ui/components/Button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@formbricks/ui/components/Card"; +import { Textarea } from "@formbricks/ui/components/Textarea"; + +interface FormbricksAICardProps { + environmentId: string; +} + +export const FormbricksAICard = ({ environmentId }: FormbricksAICardProps) => { + const router = useRouter(); + const [aiPrompt, setAiPrompt] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + // Here you would typically send the data to your backend + const createSurveyResponse = await createAISurveyAction({ + environmentId, + prompt: aiPrompt, + }); + + if (createSurveyResponse?.data) { + router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit`); + } else { + const errorMessage = getFormattedErrorMessage(createSurveyResponse); + toast.error(errorMessage); + } + // Reset form field after submission + setAiPrompt(""); + setIsLoading(false); + }; + + return ( + + + Formbricks AI + + Describe your survey and let Formbricks AI create the survey for you + + + +
+