diff --git a/apps/web/src/actions/workspaceOnboarding/calculateNavbarSteps.ts b/apps/web/src/actions/workspaceOnboarding/calculateNavbarSteps.ts new file mode 100644 index 0000000000..737053941a --- /dev/null +++ b/apps/web/src/actions/workspaceOnboarding/calculateNavbarSteps.ts @@ -0,0 +1,15 @@ +'use server' + +import { authProcedure } from '../procedures' +import { calculateAllSteps } from '@latitude-data/core/services/workspaceOnboarding/steps/calculateAllSteps' + +/** + * Calculate all steps for the onboarding + */ +export const calculateNavbarStepsAction = authProcedure.action( + async ({ ctx }) => { + return await calculateAllSteps({ + workspace: ctx.workspace, + }).then((r) => r.unwrap()) + }, +) diff --git a/apps/web/src/actions/workspaceOnboarding/moveNextStep.ts b/apps/web/src/actions/workspaceOnboarding/moveNextStep.ts index e7ba86909c..ffa36f13c5 100644 --- a/apps/web/src/actions/workspaceOnboarding/moveNextStep.ts +++ b/apps/web/src/actions/workspaceOnboarding/moveNextStep.ts @@ -3,12 +3,15 @@ import { getWorkspaceOnboarding } from '@latitude-data/core/services/workspaceOnboarding/get' import { authProcedure } from '../procedures' import { moveNextOnboardingStep } from '@latitude-data/core/services/workspaceOnboarding/steps/moveNextOnboardingStep' +import { z } from 'zod' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' /** * Move to the next onboarding step */ -export const moveNextOnboardingStepAction = authProcedure.action( - async ({ ctx }) => { +export const moveNextOnboardingStepAction = authProcedure + .inputSchema(z.object({ currentStep: z.nativeEnum(OnboardingStepKey) })) + .action(async ({ parsedInput, ctx }) => { const onboarding = await getWorkspaceOnboarding({ workspace: ctx.workspace, }).then((r) => r.unwrap()) @@ -16,8 +19,8 @@ export const moveNextOnboardingStepAction = authProcedure.action( const nextOnboardingStep = await moveNextOnboardingStep({ onboarding, workspace: ctx.workspace, + currentStep: parsedInput.currentStep, }).then((r) => r.unwrap()) return nextOnboardingStep - }, -) + }) diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/Navbar/NocodersNavbar.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/Navbar/NocodersNavbar.tsx index 0eb505d574..533927ab15 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/Navbar/NocodersNavbar.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/Navbar/NocodersNavbar.tsx @@ -13,54 +13,54 @@ import { useCurrentProject } from '@latitude-data/web-ui/providers' import { redirect } from 'next/navigation' export default function NocodersNavbar({ + onboardingSteps, currentStep, isLoadingOnboarding, executeCompleteOnboarding, }: { + onboardingSteps: OnboardingStepKey[] executeCompleteOnboarding: () => void currentStep: OnboardingStepKey | undefined | null // TODO(onboarding): remove null when data migration is done isLoadingOnboarding: boolean }) { - const project = useCurrentProject() + const { project } = useCurrentProject() const skipOnboarding = useCallback(() => { executeCompleteOnboarding() redirect(ROUTES.dashboard.root) }, [executeCompleteOnboarding]) + const ONBOARDING_STEPS = Object.entries(ONBOARDING_STEP_CONTENT) + const isLast = ONBOARDING_STEPS.length - 1 + const filteredNavbarSteps = ONBOARDING_STEPS.filter(([key]) => + onboardingSteps.includes(key as OnboardingStepKey), + ) + return (
Create your first agent - {project.project.name} + {project.name}
- {Object.entries(ONBOARDING_STEP_CONTENT).map( - ([key, item], index) => ( - -
- -
- {index === - Object.entries(ONBOARDING_STEP_CONTENT).length - 1 ? null : ( - - )} -
- ), - )} + {filteredNavbarSteps.map(([key, item], index) => ( + +
+ +
+ {index === isLast ? null : } +
+ ))}
diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/ConfiguredTriggers.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/ConfiguredTriggers.tsx new file mode 100644 index 0000000000..159199b195 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/ConfiguredTriggers.tsx @@ -0,0 +1,94 @@ +import { useMemo } from 'react' +import { + DocumentTrigger, + DocumentVersion, + IntegrationDto, +} from '@latitude-data/core/schema/types' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { cn } from '@latitude-data/web-ui/utils' +import { useTriggerInfo } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersCard' +import useDocumentVersions from '$/stores/documentVersions' +import { + StatusFlag, + StatusFlagState, +} from '@latitude-data/web-ui/molecules/StatusFlag' +import { useCurrentCommit } from '@latitude-data/web-ui/providers' + +export function ConfiguredTriggers({ + trigger, + integrations, +}: { + trigger: DocumentTrigger + integrations: IntegrationDto[] +}) { + const { commit } = useCurrentCommit() + const { data: documents } = useDocumentVersions({ + projectId: trigger.projectId, + commitUuid: commit.uuid, + }) + + const document = useMemo( + () => documents?.find((d) => d.documentUuid === trigger.documentUuid), + [documents, trigger.documentUuid], + ) + + // Loading documents. Triggers always should have a document linked + if (!document) return null + + return ( + + ) +} + +function ConfiguredTriggerWrapper({ + trigger, + document, + integrations, +}: { + trigger: DocumentTrigger + document: DocumentVersion + integrations: IntegrationDto[] +}) { + const { image, title, integration } = useTriggerInfo({ + trigger, + document, + integrations, + }) + + return ( +
+
+
+
+ {image} +
+
+ +
+
+
+ + {title} + + {integration && ( + + {integration.configuration.metadata?.displayName} trigger + configured successfully + + )} +
+
+
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/UnconfiguredTriggers.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/UnconfiguredTriggers.tsx new file mode 100644 index 0000000000..56a8e823c7 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/_components/UnconfiguredTriggers.tsx @@ -0,0 +1,108 @@ +import { useCallback, useMemo, useState } from 'react' +import { + DocumentTrigger, + DocumentVersion, + IntegrationDto, +} from '@latitude-data/core/schema/types' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { cn } from '@latitude-data/web-ui/utils' +import { useTriggerInfo } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersCard' +import useDocumentVersions from '$/stores/documentVersions' +import { Button } from '@latitude-data/web-ui/atoms/Button' +import { EditTriggerModal } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/@modal/(.)triggers/[triggerUuid]/edit/EditTriggerModal' +import { useCurrentCommit } from '@latitude-data/web-ui/providers' + +export function UnconfiguredTriggers({ + trigger, + integrations, +}: { + trigger: DocumentTrigger + integrations: IntegrationDto[] +}) { + const { commit } = useCurrentCommit() + const { data: documents } = useDocumentVersions({ + projectId: trigger.projectId, + commitUuid: commit.uuid, + }) + + const document = useMemo( + () => documents?.find((d) => d.documentUuid === trigger.documentUuid), + [documents, trigger.documentUuid], + ) + + // Loading documents. Triggers always should have a document linked + if (!document) return null + + return ( + + ) +} + +function ConfigureTriggerWrapper({ + trigger, + document, + integrations, +}: { + trigger: DocumentTrigger + document: DocumentVersion + integrations: IntegrationDto[] +}) { + const [isEditModalOpen, setIsEditModalOpen] = useState(false) + const { image, title, integration } = useTriggerInfo({ + trigger, + document, + integrations, + }) + + const onCloseModal = useCallback(() => { + setIsEditModalOpen(false) + }, []) + + return ( +
+
+
+ {image} +
+
+ + {title} + + {integration && ( + + {integration.configuration.metadata?.displayName} trigger needs + configuring + + )} +
+
+ +
+
+ {isEditModalOpen && ( + + )} +
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/index.tsx new file mode 100644 index 0000000000..6fc90f1823 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/ConfigureTriggers/index.tsx @@ -0,0 +1,162 @@ +import { Fragment, useCallback, useMemo } from 'react' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { Button } from '@latitude-data/web-ui/atoms/Button' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' +import useDocumentTriggers from '$/stores/documentTriggers' +import useIntegrations from '$/stores/integrations' +import { UnconfiguredTriggers } from './_components/UnconfiguredTriggers' +import { + useCurrentCommit, + useCurrentProject, +} from '@latitude-data/web-ui/providers' +import { + DocumentTriggerStatus, + DocumentTriggerType, +} from '@latitude-data/constants' +import { ConfiguredTriggers } from './_components/ConfiguredTriggers' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { OnboardingStep } from '../../../lib/OnboardingStep' + +export function ConfigureTriggersHeader() { + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + + const { data: triggers } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, + }) + + const sortedIntegrationTriggersByPendingFirst = useMemo(() => { + return triggers + .filter( + (trigger) => trigger.triggerType === DocumentTriggerType.Integration, + ) + .sort((a) => { + return a.triggerStatus === DocumentTriggerStatus.Pending ? -1 : 1 + }) + }, [triggers]) + + const allUnconfiguredTriggers = useMemo(() => { + return sortedIntegrationTriggersByPendingFirst.filter( + (trigger) => trigger.triggerStatus === DocumentTriggerStatus.Pending, + ) + }, [sortedIntegrationTriggersByPendingFirst]) + + const allTriggersConfigured = useMemo(() => { + return allUnconfiguredTriggers.length === 0 + }, [allUnconfiguredTriggers]) + + return ( + +
+ +
+ {!allTriggersConfigured ? ( + <> + + Configure{' '} + + {allUnconfiguredTriggers.length} + {' '} + agent triggers + + + Some triggers may require additional configuration + + + ) : ( + <> + + All triggers configured! + + + You can proceed to the next step + + + )} +
+ ) +} + +export function ConfigureTriggersBody({ + moveNextOnboardingStep, +}: { + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void +}) { + const handleNext = useCallback(() => { + moveNextOnboardingStep({ currentStep: OnboardingStepKey.ConfigureTriggers }) + }, [moveNextOnboardingStep]) + + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + + const { data: triggers, isLoading: isLoadingTriggers } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, + }) + + const sortedIntegrationTriggersByPendingFirst = useMemo(() => { + return triggers + .filter( + (trigger) => trigger.triggerType === DocumentTriggerType.Integration, + ) + .sort((a) => { + return a.triggerStatus === DocumentTriggerStatus.Pending ? -1 : 1 + }) + }, [triggers]) + + const allUnconfiguredTriggers = useMemo(() => { + return sortedIntegrationTriggersByPendingFirst.filter( + (trigger) => trigger.triggerStatus === DocumentTriggerStatus.Pending, + ) + }, [sortedIntegrationTriggersByPendingFirst]) + + const allTriggersConfigured = useMemo(() => { + return allUnconfiguredTriggers.length === 0 + }, [allUnconfiguredTriggers]) + + return ( + +
+ {isLoadingTriggers || isLoadingIntegrations ? ( + + ) : ( + sortedIntegrationTriggersByPendingFirst.map((trigger) => ( + + {trigger.triggerStatus === DocumentTriggerStatus.Pending && ( + + )} + {trigger.triggerStatus === DocumentTriggerStatus.Deployed && ( + + )} + + )) + )} +
+ +
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/RunAgent/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/RunAgent/index.tsx new file mode 100644 index 0000000000..6accbe4940 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/RunAgent/index.tsx @@ -0,0 +1,134 @@ +import { useCallback, useRef, useMemo } from 'react' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' +import { Button } from '@latitude-data/web-ui/atoms/Button' +import Chat from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat' +import { ActiveTrigger } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' +import { redirect } from 'next/navigation' +import { ROUTES } from '$/services/routes' +import { + useCurrentCommit, + useCurrentProject, +} from '@latitude-data/web-ui/providers' +import { useAutoScroll } from '@latitude-data/web-ui/hooks/useAutoScroll' +import { StatusIndicator } from '$/components/PlaygroundCommon/StatusIndicator' +import { OnboardingStep } from '../../../lib/OnboardingStep' +import { usePlayground } from '../../../lib/PlaygroundProvider' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' + +export function RunAgentHeader() { + const { playground, hasActiveStream } = usePlayground() + + const promptIsRunning = useMemo(() => { + return playground.isLoading || !playground.error || !hasActiveStream() + }, [playground.isLoading, playground.error, hasActiveStream]) + + return ( + + {promptIsRunning ? ( +
+ +
+ ) : ( +
+ +
+ )} + {promptIsRunning ? ( + <> + + Now watch your agent run + + + Once triggered, agent will perform actions and call sub-agents by + itself + + + ) : ( + <> + + Done! + + Now watch your agent run + + )} +
+ ) +} + +export function RunAgentBody({ + executeCompleteOnboarding, + activeTrigger, +}: { + executeCompleteOnboarding: () => void + activeTrigger: ActiveTrigger +}) { + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + const { playground, hasActiveStream, abortCurrentStream } = usePlayground() + + const handleNext = useCallback(() => { + executeCompleteOnboarding() + abortCurrentStream() + redirect( + ROUTES.projects + .detail({ id: project.id }) + .commits.detail({ uuid: commit.uuid }).preview.root, + ) + }, [executeCompleteOnboarding, project, commit, abortCurrentStream]) + + const containerRef = useRef(null) + + useAutoScroll(containerRef, { startAtBottom: true }) + + const promptIsRunning = useMemo(() => { + return playground.isLoading || !playground.error || !hasActiveStream() + }, [playground.isLoading, playground.error, hasActiveStream]) + + return ( + +
+ {playground.messages.length === 0 && !playground.error ? ( + + ) : ( + <> + +
+
+ {!playground.error && ( + {}} + stopStreaming={() => {}} + canStopStreaming={false} + streamAborted={false} + canChat={false} + position='bottom' + /> + )} +
+
+ + )} +
+ +
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/SetupIntegrations/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/SetupIntegrations/index.tsx new file mode 100644 index 0000000000..6ba5f55b9e --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/SetupIntegrations/index.tsx @@ -0,0 +1,99 @@ +import { useCallback, useMemo } from 'react' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { Button } from '@latitude-data/web-ui/atoms/Button' +import useIntegrations from '$/stores/integrations' +import { + getPipedreamUnconfiguredIntegrations, + UnconfiguredIntegrations, +} from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/UnconfiguredIntegrations' +import { ConfiguredIntegrations } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/ConfiguredIntegrations' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { OnboardingStep } from '../../../lib/OnboardingStep' + +export function SetupIntegrationsHeader() { + const { data: integrations } = useIntegrations() + + const allIntegrationsConfigured = useMemo(() => { + return getPipedreamUnconfiguredIntegrations(integrations).length === 0 + }, [integrations]) + + return ( + +
+ +
+ {!allIntegrationsConfigured ? ( + <> + + Set up{' '} + + {getPipedreamUnconfiguredIntegrations(integrations).length} + {' '} + integrations + + + Integrations allow your agent to connect to other apps + + + ) : ( + <> + + All integrations configured! + + + You can proceed to the next step + + + )} +
+ ) +} + +export function SetupIntegrationsBody({ + moveNextOnboardingStep, +}: { + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void +}) { + const handleNext = useCallback(() => { + moveNextOnboardingStep({ currentStep: OnboardingStepKey.SetupIntegrations }) + }, [moveNextOnboardingStep]) + + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() + + const allIntegrationsConfigured = useMemo(() => { + return getPipedreamUnconfiguredIntegrations(integrations).length === 0 + }, [integrations]) + + return ( + +
+ {isLoadingIntegrations ? ( + + ) : ( + <> + + + + )} +
+ +
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/_components/RunTrigger.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/_components/RunTrigger.tsx new file mode 100644 index 0000000000..0987881ded --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/_components/RunTrigger.tsx @@ -0,0 +1,175 @@ +import { DocumentTriggerType } from '@latitude-data/constants' +import { + DocumentTrigger, + DocumentVersion, + IntegrationDto, +} from '@latitude-data/core/schema/types' +import { Button } from '@latitude-data/web-ui/atoms/Button' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { cn } from '@latitude-data/web-ui/utils' +import { useCallback, useMemo } from 'react' +import { TriggerEventsList } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggerEventsList' +import { OnRunTriggerFn } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' +import { useTriggerInfo } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersCard' +import { useCurrentCommit } from '@latitude-data/web-ui/providers' +import useDocumentVersions from '$/stores/documentVersions' +import { + isChatTrigger, + RUNNABLE_TRIGGERS, +} from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggerWrapper' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' + +function isIntegrationTrigger( + trigger: DocumentTrigger, +): trigger is DocumentTrigger { + return trigger.triggerType === DocumentTriggerType.Integration +} + +export function RunTrigger({ + trigger, + integrations, + onRunTrigger, + onRunChatTrigger, +}: { + trigger: DocumentTrigger + integrations: IntegrationDto[] + onRunTrigger: OnRunTriggerFn + onRunChatTrigger: () => void +}) { + const { commit } = useCurrentCommit() + const { data: documents } = useDocumentVersions({ + projectId: trigger.projectId, + commitUuid: commit.uuid, + }) + + const document = useMemo( + () => documents?.find((d) => d.documentUuid === trigger.documentUuid), + [documents, trigger.documentUuid], + ) + + // Loading documents. Triggers always should have a document linked + if (!document) return null + + return ( + + ) +} + +export function RunTriggerWrapper({ + trigger, + integrations, + document, + onRunTrigger, + onRunChatTrigger, +}: { + trigger: DocumentTrigger + integrations: IntegrationDto[] + document: DocumentVersion + onRunTrigger: OnRunTriggerFn + onRunChatTrigger: () => void +}) { + const { image, title, description } = useTriggerInfo({ + trigger, + document, + integrations, + }) + const canRunTrigger = RUNNABLE_TRIGGERS.includes(trigger.triggerType) + + const handleRunTrigger = useCallback(() => { + if (isIntegrationTrigger(trigger)) { + onRunTrigger({ document, parameters: {}, aiParameters: true }) + return + } + + if (isChatTrigger(trigger)) { + onRunChatTrigger() + return + } + + // Schedule triggers don't have parameters + onRunTrigger({ document, parameters: {} }) + }, [onRunTrigger, onRunChatTrigger, trigger, document]) + + return ( +
+
+
+
+
+ {image} +
+
+
+
+ + {title} + + {description ? ( + + {description} + + ) : null} +
+
+
+ {canRunTrigger ? ( + + ) : null} + {trigger.triggerType === DocumentTriggerType.Integration ? ( + + ) : null} +
+ {!RUNNABLE_TRIGGERS.includes(trigger.triggerType) ? ( + } + /> + ) : null} +
+ ) +} + +function TriggerEventsEmptyState({ title }: { title: string }) { + return ( +
+
+ + Waiting for events... + + To use this trigger to preview your agent, perform the action that + triggers {title}. + +
+
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/index.tsx new file mode 100644 index 0000000000..6b2e0860cd --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/TriggerAgent/index.tsx @@ -0,0 +1,145 @@ +import { useCallback, useMemo, useState } from 'react' +import { Text } from '@latitude-data/web-ui/atoms/Text' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' +import useDocumentTriggers from '$/stores/documentTriggers' +import { + useCurrentCommit, + useCurrentProject, +} from '@latitude-data/web-ui/providers' +import { DocumentTriggerType } from '@latitude-data/constants' +import { RunTrigger } from './_components/RunTrigger' +import useIntegrations from '$/stores/integrations' +import { ChatTriggerTextarea } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/ChatTriggerTextarea' +import { useActiveChatTrigger } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/useActiveTrigger' +import { + ActiveTrigger, + OnRunTriggerFn, +} from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { useTriggerSockets } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/useTriggerSockets' +import { OnboardingStep } from '../../../lib/OnboardingStep' +import { usePlayground } from '../../../lib/PlaygroundProvider' + +export function TriggerAgentHeader() { + return ( + +
+ +
+ + Trigger the agent + + + Perform one of the below actions to trigger and run the agent + +
+ ) +} + +export function TriggerAgentBody({ + moveNextOnboardingStep, + setActiveTrigger, +}: { + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void + setActiveTrigger: (trigger: ActiveTrigger) => void +}) { + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + const { playground } = usePlayground() + + const { + data: triggers, + isLoading: isLoadingTriggers, + mutate, + } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, + }) + + useTriggerSockets({ commit: commit, project: project, mutate }) + + const activeChatTrigger = useActiveChatTrigger({ + commit: commit, + project: project, + triggers, + }) + + const [openChatInput, setOpenChatInput] = useState(false) + const toggleOpenChatInput = useCallback(() => { + if (openChatInput) { + setOpenChatInput(false) + } else { + setOpenChatInput(true) + } + }, [openChatInput]) + + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() + + const sortedTriggersByIntegrationFirst = useMemo(() => { + return triggers.sort((a) => { + return a.triggerType === DocumentTriggerType.Integration ? -1 : 1 + }) + }, [triggers]) + + const onRunTrigger: OnRunTriggerFn = useCallback( + ({ document, parameters, userMessage, aiParameters = false }) => { + setActiveTrigger({ document, parameters, userMessage }) + playground.start({ document, parameters, userMessage, aiParameters }) + moveNextOnboardingStep({ currentStep: OnboardingStepKey.TriggerAgent }) + }, + [setActiveTrigger, moveNextOnboardingStep, playground], + ) + + return ( + +
+ {isLoadingTriggers || isLoadingIntegrations ? ( + + ) : ( + sortedTriggersByIntegrationFirst.map((trigger) => ( + + )) + )} +
+
+ {activeChatTrigger.active && openChatInput ? ( +
+ +
+ ) : null} +
+ + Agent will start running automatically +
+ once you trigger it +
+
+
+
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/configureTriggers/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/configureTriggers/index.tsx index 1158beff14..6fc90f1823 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/configureTriggers/index.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/configureTriggers/index.tsx @@ -14,23 +14,92 @@ import { DocumentTriggerType, } from '@latitude-data/constants' import { ConfiguredTriggers } from './_components/ConfiguredTriggers' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { OnboardingStep } from '../../../lib/OnboardingStep' -export function ConfigureTriggersStep({ +export function ConfigureTriggersHeader() { + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + + const { data: triggers } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, + }) + + const sortedIntegrationTriggersByPendingFirst = useMemo(() => { + return triggers + .filter( + (trigger) => trigger.triggerType === DocumentTriggerType.Integration, + ) + .sort((a) => { + return a.triggerStatus === DocumentTriggerStatus.Pending ? -1 : 1 + }) + }, [triggers]) + + const allUnconfiguredTriggers = useMemo(() => { + return sortedIntegrationTriggersByPendingFirst.filter( + (trigger) => trigger.triggerStatus === DocumentTriggerStatus.Pending, + ) + }, [sortedIntegrationTriggersByPendingFirst]) + + const allTriggersConfigured = useMemo(() => { + return allUnconfiguredTriggers.length === 0 + }, [allUnconfiguredTriggers]) + + return ( + +
+ +
+ {!allTriggersConfigured ? ( + <> + + Configure{' '} + + {allUnconfiguredTriggers.length} + {' '} + agent triggers + + + Some triggers may require additional configuration + + + ) : ( + <> + + All triggers configured! + + + You can proceed to the next step + + + )} +
+ ) +} + +export function ConfigureTriggersBody({ moveNextOnboardingStep, }: { - moveNextOnboardingStep: () => void + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void }) { const handleNext = useCallback(() => { - moveNextOnboardingStep() + moveNextOnboardingStep({ currentStep: OnboardingStepKey.ConfigureTriggers }) }, [moveNextOnboardingStep]) - const { data: integrations } = useIntegrations() - const project = useCurrentProject() - const commit = useCurrentCommit() + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() - const { data: triggers } = useDocumentTriggers({ - projectId: project.project.id, - commitUuid: commit.commit.uuid, + const { data: triggers, isLoading: isLoadingTriggers } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, }) const sortedIntegrationTriggersByPendingFirst = useMemo(() => { @@ -54,53 +123,32 @@ export function ConfigureTriggersStep({ }, [allUnconfiguredTriggers]) return ( -
-
-
- -
- {!allTriggersConfigured ? ( - <> - - Configure{' '} - - {allUnconfiguredTriggers.length} - {' '} - agent triggers - - - Some triggers may require additional configuration - - + +
+ {isLoadingTriggers || isLoadingIntegrations ? ( + ) : ( - <> - - All triggers configured! - - - You can proceed to the next step - - + sortedIntegrationTriggersByPendingFirst.map((trigger) => ( + + {trigger.triggerStatus === DocumentTriggerStatus.Pending && ( + + )} + {trigger.triggerStatus === DocumentTriggerStatus.Deployed && ( + + )} + + )) )}
-
- {sortedIntegrationTriggersByPendingFirst.map((trigger) => ( - - {trigger.triggerStatus === DocumentTriggerStatus.Pending && ( - - )} - {trigger.triggerStatus === DocumentTriggerStatus.Deployed && ( - - )} - - ))} -
-
+ ) } diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/index.tsx index d66714b1d6..512e0db555 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/index.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/index.tsx @@ -1,36 +1,41 @@ 'use client' import NocodersNavbar from '../Navbar/NocodersNavbar' -import { SetupIntegrationsStep } from './setupIntegrations' +import { + SetupIntegrationsHeader, + SetupIntegrationsBody, +} from './SetupIntegrations' import useWorkspaceOnboarding from '$/stores/workspaceOnboarding' import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' -import { ConfigureTriggersStep } from './configureTriggers' -import { TriggerAgentStep } from './triggerAgent' -import { useCallback, useRef, useState } from 'react' -import { useAutoScroll } from '@latitude-data/web-ui/hooks/useAutoScroll' -import { RunAgentStep } from './runAgent' +import { + ConfigureTriggersHeader, + ConfigureTriggersBody, +} from './ConfigureTriggers' +import { TriggerAgentHeader, TriggerAgentBody } from './TriggerAgent' +import { useState } from 'react' +import { RunAgentHeader, RunAgentBody } from './RunAgent' import { ActiveTrigger, FAKE_DOCUMENT, } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' -import { useRunDocument } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/hooks/useRunDocument' -import { useCurrentCommit } from '@latitude-data/web-ui/providers' -import { usePlaygroundChat } from '$/hooks/playgroundChat/usePlaygroundChat' -import { DocumentVersion } from '@latitude-data/core/schema/types' +import { OnboardingStep } from '$/app/(onboarding)/onboarding/lib/OnboardingStep' +import { PlaygroundProvider } from '../../lib/PlaygroundProvider' -export function OnboardingClient() { +export function OnboardingClient({ + onboardingSteps, +}: { + onboardingSteps: OnboardingStepKey[] +}) { const { - onboarding: currentOnboarding, + onboarding, moveNextOnboardingStep, isLoading: isLoadingOnboarding, executeCompleteOnboarding, } = useWorkspaceOnboarding() - const currentStep = currentOnboarding?.currentStep - - const containerRef = useRef(null) - - useAutoScroll(containerRef, { startAtBottom: true }) + const currentStep = onboarding?.currentStep + ? onboarding?.currentStep + : onboardingSteps[0] const [activeTrigger, setActiveTrigger] = useState({ document: FAKE_DOCUMENT, @@ -38,25 +43,29 @@ export function OnboardingClient() { }) return ( -
+
-
+
{currentStep === OnboardingStepKey.SetupIntegrations && ( - + + + + )} {currentStep === OnboardingStepKey.ConfigureTriggers && ( - + + + + )} {(currentStep === OnboardingStepKey.TriggerAgent || currentStep === OnboardingStepKey.RunAgent) && ( @@ -80,63 +89,36 @@ function PlaygroundSteps({ executeCompleteOnboarding, activeTrigger, }: { - moveNextOnboardingStep: () => void + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void setActiveTrigger: (trigger: ActiveTrigger) => void currentStep: OnboardingStepKey executeCompleteOnboarding: () => void activeTrigger: ActiveTrigger }) { - const commit = useCurrentCommit() - - const { runDocument, addMessages } = useRunDocument({ - commit: commit.commit, - }) - - const runPromptFn = useCallback( - ({ - document, - userMessage, - parameters = {}, - aiParameters = true, - }: { - document: DocumentVersion - parameters: Record - userMessage: string | undefined - aiParameters: boolean - }) => - runDocument({ - document, - parameters, - userMessage, - aiParameters, - }), - [runDocument], - ) - - const playground = usePlaygroundChat({ - runPromptFn, - addMessagesFn: addMessages, - onPromptRan: (documentLogUuid, error) => { - if (!documentLogUuid || error) return - }, - }) - return ( - <> + {currentStep === OnboardingStepKey.TriggerAgent && ( - + + + + )} {currentStep === OnboardingStepKey.RunAgent && ( - + + + + )} - + ) } diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/runAgent/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/runAgent/index.tsx index 7a5fb86b47..6accbe4940 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/runAgent/index.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/runAgent/index.tsx @@ -1,63 +1,134 @@ -import { useCallback } from 'react' +import { useCallback, useRef, useMemo } from 'react' import { Text } from '@latitude-data/web-ui/atoms/Text' import { Icon } from '@latitude-data/web-ui/atoms/Icons' import { Button } from '@latitude-data/web-ui/atoms/Button' import Chat from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat' import { ActiveTrigger } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' -import { usePlaygroundChat } from '$/hooks/playgroundChat/usePlaygroundChat' import { redirect } from 'next/navigation' import { ROUTES } from '$/services/routes' import { useCurrentCommit, useCurrentProject, } from '@latitude-data/web-ui/providers' +import { useAutoScroll } from '@latitude-data/web-ui/hooks/useAutoScroll' +import { StatusIndicator } from '$/components/PlaygroundCommon/StatusIndicator' +import { OnboardingStep } from '../../../lib/OnboardingStep' +import { usePlayground } from '../../../lib/PlaygroundProvider' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' -export function RunAgentStep({ - moveNextOnboardingStep, +export function RunAgentHeader() { + const { playground, hasActiveStream } = usePlayground() + + const promptIsRunning = useMemo(() => { + return playground.isLoading || !playground.error || !hasActiveStream() + }, [playground.isLoading, playground.error, hasActiveStream]) + + return ( + + {promptIsRunning ? ( +
+ +
+ ) : ( +
+ +
+ )} + {promptIsRunning ? ( + <> + + Now watch your agent run + + + Once triggered, agent will perform actions and call sub-agents by + itself + + + ) : ( + <> + + Done! + + Now watch your agent run + + )} +
+ ) +} + +export function RunAgentBody({ + executeCompleteOnboarding, activeTrigger, - playground, }: { - moveNextOnboardingStep: () => void + executeCompleteOnboarding: () => void activeTrigger: ActiveTrigger - playground: ReturnType }) { const { project } = useCurrentProject() const { commit } = useCurrentCommit() + const { playground, hasActiveStream, abortCurrentStream } = usePlayground() const handleNext = useCallback(() => { - moveNextOnboardingStep() + executeCompleteOnboarding() + abortCurrentStream() redirect( ROUTES.projects .detail({ id: project.id }) .commits.detail({ uuid: commit.uuid }).preview.root, ) - }, [moveNextOnboardingStep, project, commit]) + }, [executeCompleteOnboarding, project, commit, abortCurrentStream]) + + const containerRef = useRef(null) + + useAutoScroll(containerRef, { startAtBottom: true }) + + const promptIsRunning = useMemo(() => { + return playground.isLoading || !playground.error || !hasActiveStream() + }, [playground.isLoading, playground.error, hasActiveStream]) return ( -
-
-
- -
- - Done! - - Your agent is ready-to-go! -
-
- + +
+ {playground.messages.length === 0 && !playground.error ? ( + + ) : ( + <> + +
+
+ {!playground.error && ( + {}} + stopStreaming={() => {}} + canStopStreaming={false} + streamAborted={false} + canChat={false} + position='bottom' + /> + )} +
+
+ + )}
-
+ ) } diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/setupIntegrations/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/setupIntegrations/index.tsx index a8a7096586..6ba5f55b9e 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/setupIntegrations/index.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/setupIntegrations/index.tsx @@ -8,56 +8,84 @@ import { UnconfiguredIntegrations, } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/UnconfiguredIntegrations' import { ConfiguredIntegrations } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/ConfiguredIntegrations' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { OnboardingStep } from '../../../lib/OnboardingStep' -export function SetupIntegrationsStep({ +export function SetupIntegrationsHeader() { + const { data: integrations } = useIntegrations() + + const allIntegrationsConfigured = useMemo(() => { + return getPipedreamUnconfiguredIntegrations(integrations).length === 0 + }, [integrations]) + + return ( + +
+ +
+ {!allIntegrationsConfigured ? ( + <> + + Set up{' '} + + {getPipedreamUnconfiguredIntegrations(integrations).length} + {' '} + integrations + + + Integrations allow your agent to connect to other apps + + + ) : ( + <> + + All integrations configured! + + + You can proceed to the next step + + + )} +
+ ) +} + +export function SetupIntegrationsBody({ moveNextOnboardingStep, }: { - moveNextOnboardingStep: () => void + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void }) { const handleNext = useCallback(() => { - moveNextOnboardingStep() + moveNextOnboardingStep({ currentStep: OnboardingStepKey.SetupIntegrations }) }, [moveNextOnboardingStep]) - const { data: integrations } = useIntegrations() + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() const allIntegrationsConfigured = useMemo(() => { return getPipedreamUnconfiguredIntegrations(integrations).length === 0 }, [integrations]) return ( -
-
-
- -
- {!allIntegrationsConfigured ? ( - <> - - Set up{' '} - - {getPipedreamUnconfiguredIntegrations(integrations).length} - {' '} - integrations - - - Integrations allow your agent to connect to other apps - - + +
+ {isLoadingIntegrations ? ( + ) : ( <> - - All integrations configured! - - - You can proceed to the next step - + + )}
-
- - -
-
+ ) } diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/_components/RunTrigger.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/_components/RunTrigger.tsx index 837d9971c9..0987881ded 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/_components/RunTrigger.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/_components/RunTrigger.tsx @@ -17,6 +17,7 @@ import { isChatTrigger, RUNNABLE_TRIGGERS, } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggerWrapper' +import { Icon } from '@latitude-data/web-ui/atoms/Icons' function isIntegrationTrigger( trigger: DocumentTrigger, @@ -160,11 +161,15 @@ export function RunTriggerWrapper({ function TriggerEventsEmptyState({ title }: { title: string }) { return ( -
- - To use this trigger to preview your agent, perform the action that - triggers {title}. - +
+
+ + Waiting for events... + + To use this trigger to preview your agent, perform the action that + triggers {title}. + +
) } diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/index.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/index.tsx index 74323a7ec5..6b2e0860cd 100644 --- a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/index.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingClient/triggerAgent/index.tsx @@ -15,17 +15,60 @@ import { ActiveTrigger, OnRunTriggerFn, } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/TriggersList' -import { usePlaygroundChat } from '$/hooks/playgroundChat/usePlaygroundChat' +import { IsLoadingOnboardingItem } from '../../../lib/IsLoadingOnboardingItem' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { useTriggerSockets } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/preview/_components/useTriggerSockets' +import { OnboardingStep } from '../../../lib/OnboardingStep' +import { usePlayground } from '../../../lib/PlaygroundProvider' -export function TriggerAgentStep({ +export function TriggerAgentHeader() { + return ( + +
+ +
+ + Trigger the agent + + + Perform one of the below actions to trigger and run the agent + +
+ ) +} + +export function TriggerAgentBody({ moveNextOnboardingStep, setActiveTrigger, - playground, }: { - moveNextOnboardingStep: () => void + moveNextOnboardingStep: ({ + currentStep, + }: { + currentStep: OnboardingStepKey + }) => void setActiveTrigger: (trigger: ActiveTrigger) => void - playground: ReturnType }) { + const { project } = useCurrentProject() + const { commit } = useCurrentCommit() + const { playground } = usePlayground() + + const { + data: triggers, + isLoading: isLoadingTriggers, + mutate, + } = useDocumentTriggers({ + projectId: project.id, + commitUuid: commit.uuid, + }) + + useTriggerSockets({ commit: commit, project: project, mutate }) + + const activeChatTrigger = useActiveChatTrigger({ + commit: commit, + project: project, + triggers, + }) + const [openChatInput, setOpenChatInput] = useState(false) const toggleOpenChatInput = useCallback(() => { if (openChatInput) { @@ -35,20 +78,8 @@ export function TriggerAgentStep({ } }, [openChatInput]) - const { data: integrations } = useIntegrations() - const project = useCurrentProject() - const commit = useCurrentCommit() - - const { data: triggers } = useDocumentTriggers({ - projectId: project.project.id, - commitUuid: commit.commit.uuid, - }) - - const activeChatTrigger = useActiveChatTrigger({ - commit: commit.commit, - project: project.project, - triggers, - }) + const { data: integrations, isLoading: isLoadingIntegrations } = + useIntegrations() const sortedTriggersByIntegrationFirst = useMemo(() => { return triggers.sort((a) => { @@ -60,45 +91,41 @@ export function TriggerAgentStep({ ({ document, parameters, userMessage, aiParameters = false }) => { setActiveTrigger({ document, parameters, userMessage }) playground.start({ document, parameters, userMessage, aiParameters }) - moveNextOnboardingStep() + moveNextOnboardingStep({ currentStep: OnboardingStepKey.TriggerAgent }) }, [setActiveTrigger, moveNextOnboardingStep, playground], ) return ( -
-
-
- -
- - Trigger the agent - - - Perform one of the below actions to trigger and run the agent - -
-
- {sortedTriggersByIntegrationFirst.map((trigger) => ( - +
+ {isLoadingTriggers || isLoadingIntegrations ? ( + - ))} + ) : ( + sortedTriggersByIntegrationFirst.map((trigger) => ( + + )) + )}
-
+
{activeChatTrigger.active && openChatInput ? (
-
+ ) } diff --git a/apps/web/src/app/(onboarding)/onboarding/lib/IsLoadingOnboardingItem.tsx b/apps/web/src/app/(onboarding)/onboarding/lib/IsLoadingOnboardingItem.tsx new file mode 100644 index 0000000000..e153aaef51 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/lib/IsLoadingOnboardingItem.tsx @@ -0,0 +1,18 @@ +import { Text } from '@latitude-data/web-ui/atoms/Text' + +export function IsLoadingOnboardingItem({ + highlightedText, + nonHighlightedText, +}: { + highlightedText: string + nonHighlightedText: string +}) { + return ( + + + {highlightedText} + {' '} + {nonHighlightedText} + + ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/lib/OnboardingStep.tsx b/apps/web/src/app/(onboarding)/onboarding/lib/OnboardingStep.tsx new file mode 100644 index 0000000000..6a03d70c02 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/lib/OnboardingStep.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react' + +function OnboardingStepRoot({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function OnboardingStepBody({ children }: { children: ReactNode }) { + return
{children}
+} + +function OnboardingStepHeader({ children }: { children: ReactNode }) { + return
{children}
+} + +export const OnboardingStep = { + Root: OnboardingStepRoot, + Body: OnboardingStepBody, + Header: OnboardingStepHeader, +} diff --git a/apps/web/src/app/(onboarding)/onboarding/lib/PlaygroundProvider.tsx b/apps/web/src/app/(onboarding)/onboarding/lib/PlaygroundProvider.tsx new file mode 100644 index 0000000000..a714f9dc41 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/lib/PlaygroundProvider.tsx @@ -0,0 +1,79 @@ +import { createContext, useContext, useCallback, ReactNode } from 'react' +import { useCurrentCommit } from '@latitude-data/web-ui/providers' +import { useRunDocument } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/hooks/useRunDocument' +import { usePlaygroundChat } from '$/hooks/playgroundChat/usePlaygroundChat' +import { DocumentVersion } from '@latitude-data/core/schema/types' + +interface IPlaygroundContextType { + playground: ReturnType + addMessages: ReturnType['addMessages'] + hasActiveStream: ReturnType['hasActiveStream'] + runDocument: ReturnType['runDocument'] + abortCurrentStream: ReturnType['abortCurrentStream'] +} + +const PlaygroundContext = createContext( + {} as IPlaygroundContextType, +) + +const PlaygroundProvider = ({ children }: { children: ReactNode }) => { + const commit = useCurrentCommit() + + const { runDocument, addMessages, hasActiveStream, abortCurrentStream } = + useRunDocument({ + commit: commit.commit, + }) + + const runPromptFn = useCallback( + ({ + document, + userMessage, + parameters = {}, + aiParameters = true, + }: { + document: DocumentVersion + parameters: Record + userMessage: string | undefined + aiParameters: boolean + }) => + runDocument({ + document, + parameters, + userMessage, + aiParameters, + }), + [runDocument], + ) + + const playground = usePlaygroundChat({ + runPromptFn, + addMessagesFn: addMessages, + onPromptRan: (documentLogUuid, error) => { + if (!documentLogUuid || error) return + }, + }) + + return ( + + {children} + + ) +} + +const usePlayground = () => { + const context = useContext(PlaygroundContext) + if (!context) { + throw new Error('usePlayground must be used within a PlaygroundProvider') + } + return context +} + +export { PlaygroundProvider, usePlayground } diff --git a/apps/web/src/app/(onboarding)/onboarding/page.tsx b/apps/web/src/app/(onboarding)/onboarding/page.tsx index 4fd28d1194..e1eb57b772 100644 --- a/apps/web/src/app/(onboarding)/onboarding/page.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/page.tsx @@ -1,4 +1,7 @@ -import { isOnboardingCompleted } from '$/data-access' +import { + isOnboardingCompleted, + getNecessaryOnboardingSteps, +} from '$/data-access' import { ROUTES } from '$/services/routes' import { OnboardingClient } from './_components/OnboardingClient' import { redirect } from 'next/navigation' @@ -8,5 +11,7 @@ export default async function NocodersPage() { if (isCompleted) { redirect(ROUTES.dashboard.root) } - return + const onboardingSteps = await getNecessaryOnboardingSteps() + + return } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat/index.tsx index eeb2488762..15446ce003 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/V2Playground/Chat/index.tsx @@ -36,7 +36,7 @@ export default function Chat({ ) return ( -
+
{showHeader && (
}) { return ( -
+
{ - console.log('Running chat trigger', trigger.uuid) activateTrigger({ trigger, document }) setChatBoxFocused(true) diff --git a/apps/web/src/components/PlaygroundCommon/StatusIndicator/index.tsx b/apps/web/src/components/PlaygroundCommon/StatusIndicator/index.tsx index d1a4258e9c..a14b2ee020 100644 --- a/apps/web/src/components/PlaygroundCommon/StatusIndicator/index.tsx +++ b/apps/web/src/components/PlaygroundCommon/StatusIndicator/index.tsx @@ -16,19 +16,22 @@ type StatusIndicatorProps = { canStopStreaming?: boolean streamAborted?: boolean canChat?: boolean + position?: 'top' | 'bottom' } export function StatusIndicator({ playground, canChat = true, + position = 'top', ...rest }: StatusIndicatorProps) { return (
{playground.isLoading && ( @@ -41,7 +44,6 @@ export function StatusIndicator({ cost={playground.cost} duration={playground.duration} /> - )} @@ -60,10 +62,12 @@ function InnerIndicator({ stopStreaming, canStopStreaming = false, streamAborted = false, + canChat = true, }: StatusIndicatorProps) { if (wakingUpIntegration) { return ( <> + Waking up {wakingUpIntegration}{' '} integration @@ -75,6 +79,7 @@ function InnerIndicator({ if (runningLatitudeTools > 0) { return ( <> + Running {runningLatitudeTools}{' '} {runningLatitudeTools === 1 ? 'tool' : 'tools'} @@ -86,6 +91,7 @@ function InnerIndicator({ if (isStreaming && canStopStreaming && stopStreaming) { return ( <> + @@ -93,9 +99,10 @@ function InnerIndicator({ ) } - if (!isStreaming) { + if (!isStreaming && !canChat) { return ( <> + {streamAborted || streamError ? ( () - .$default(() => OnboardingStepKey.SetupIntegrations), + }).$type(), ...timestamps(), }, ) diff --git a/packages/core/src/services/workspaceOnboarding/create.ts b/packages/core/src/services/workspaceOnboarding/create.ts index a5b319a4c2..ea8dadd8e9 100644 --- a/packages/core/src/services/workspaceOnboarding/create.ts +++ b/packages/core/src/services/workspaceOnboarding/create.ts @@ -1,7 +1,6 @@ import { Result } from '../../lib/Result' import Transaction from '../../lib/Transaction' import { workspaceOnboarding } from '../../schema/models/workspaceOnboarding' -import { getFirstStep } from './steps/getFirstStep' import { Workspace } from '../../schema/types' export async function createWorkspaceOnboarding( @@ -13,16 +12,10 @@ export async function createWorkspaceOnboarding( transaction = new Transaction(), ) { return transaction.call(async (tx) => { - const firstStepResult = await getFirstStep({ workspace }, tx) - if (!Result.isOk(firstStepResult)) { - return firstStepResult - } - const firstStep = firstStepResult.unwrap() const insertedOnboardings = await tx .insert(workspaceOnboarding) .values({ workspaceId: workspace.id, - currentStep: firstStep, }) .returning() diff --git a/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.test.ts b/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.test.ts new file mode 100644 index 0000000000..71f2eb91b3 --- /dev/null +++ b/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.test.ts @@ -0,0 +1,150 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { Workspace } from '../../../../src/schema/types' +import * as factories from '../../../../src/tests/factories' +import { calculateAllSteps } from './calculateAllSteps' +import { + DocumentTriggerStatus, + IntegrationType, +} from '@latitude-data/constants' + +describe('calculateAllSteps', () => { + let workspace: Workspace + + beforeEach(async () => { + const { workspace: createdWorkspace } = await factories.createWorkspace({ + onboarding: false, + }) + workspace = createdWorkspace + }) + + it('should return all steps when there are pending configuration integrations and pending triggers', async () => { + // Create a project with a merged commit + const { project, commit } = await factories.createProject({ + workspace, + documents: { + 'test.promptl': 'test content', + }, + }) + + // Create a Pipatedream integration + const integration = await factories.createIntegration({ + workspace, + type: IntegrationType.Pipedream, + configuration: { + appName: 'slack', + authType: 'oauth', + }, + }) + + // Create a pending integration trigger + await factories.createIntegrationDocumentTrigger({ + workspaceId: workspace.id, + projectId: project.id, + commitId: commit.id, + integrationId: integration.id, + triggerStatus: DocumentTriggerStatus.Pending, + }) + + const result = await calculateAllSteps({ + workspace, + }) + + expect(result.ok).toBe(true) + const steps = result.unwrap() + expect(steps).toEqual([ + OnboardingStepKey.SetupIntegrations, + OnboardingStepKey.ConfigureTriggers, + OnboardingStepKey.TriggerAgent, + OnboardingStepKey.RunAgent, + ]) + }) + + it('should skip configure triggers when there are no pending configuration integrations', async () => { + // Create a Pipedream integration + await factories.createIntegration({ + workspace, + type: IntegrationType.Pipedream, + configuration: { + appName: 'slack', + authType: 'oauth', + }, + }) + + const result = await calculateAllSteps({ + workspace, + }) + + expect(result.ok).toBe(true) + const steps = result.unwrap() + expect(steps).toEqual([ + OnboardingStepKey.SetupIntegrations, + OnboardingStepKey.TriggerAgent, + OnboardingStepKey.RunAgent, + ]) + }) + + it('should skip configuring integrations or triggers when there are no pending configuration integrations and pending triggers', async () => { + const result = await calculateAllSteps({ + workspace, + }) + + expect(result.ok).toBe(true) + const steps = result.unwrap() + expect(steps).toEqual([ + OnboardingStepKey.TriggerAgent, + OnboardingStepKey.RunAgent, + ]) + }) + + it('should skip configuring integrations or triggers when there are only latitude triggers ', async () => { + // Create a project with a merged commit + const { project, commit } = await factories.createProject({ + workspace, + documents: { + 'test.promptl': 'test content', + }, + }) + + // Create a scheduled trigger (non-integration) + await factories.createScheduledDocumentTrigger({ + workspaceId: workspace.id, + projectId: project.id, + commitId: commit.id, + }) + + const result = await calculateAllSteps({ + workspace, + }) + + expect(result.ok).toBe(true) + const steps = result.unwrap() + expect(steps).toEqual([ + OnboardingStepKey.TriggerAgent, + OnboardingStepKey.RunAgent, + ]) + }) + + it('Should set up integrations when there is an external MCP integration', async () => { + // Create an external MCP integration + await factories.createIntegration({ + workspace, + type: IntegrationType.ExternalMCP, + configuration: { + url: 'https://example.com', + }, + }) + + const result = await calculateAllSteps({ + workspace, + }) + + expect(result.ok).toBe(true) + const steps = result.unwrap() + expect(steps).toEqual([ + OnboardingStepKey.SetupIntegrations, + OnboardingStepKey.TriggerAgent, + OnboardingStepKey.RunAgent, + ]) + }) +}) diff --git a/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.ts b/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.ts new file mode 100644 index 0000000000..1b474024b7 --- /dev/null +++ b/packages/core/src/services/workspaceOnboarding/steps/calculateAllSteps.ts @@ -0,0 +1,46 @@ +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' +import { Workspace } from '../../../schema/types' +import { getNextAvailableStep } from './getNextAvailableStep' +import { Result } from '../../../lib/Result' +import { database } from '../../../client' +import { PromisedResult } from '../../../lib/Transaction' +import { getFirstStep } from './getFirstStep' +import { OnboardingCompleteError } from './onboardingCompleteError' + +export async function calculateAllSteps( + { + workspace, + }: { + workspace: Workspace + }, + db = database, +): PromisedResult { + const firstStepResult = await getFirstStep({ workspace }, db) + if (!Result.isOk(firstStepResult)) { + return firstStepResult + } + const firstStep = firstStepResult.unwrap() + + const steps: OnboardingStepKey[] = [] + steps.push(firstStep) + let workingStep = firstStep + + while (true) { + const nextStepResult = await getNextAvailableStep( + { + currentStep: workingStep, + workspace, + }, + db, + ) + if (!Result.isOk(nextStepResult)) { + if (nextStepResult.error instanceof OnboardingCompleteError) { + return Result.ok(steps) + } + return nextStepResult + } + const nextStep = nextStepResult.unwrap() + steps.push(nextStep) + workingStep = nextStep + } +} diff --git a/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.test.ts b/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.test.ts index fd019ca6ed..2a2acb03fd 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.test.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.test.ts @@ -120,6 +120,41 @@ describe('checkNextStepNecessary', () => { const resultValue = result.unwrap() expect(resultValue).toBe(true) }) + + it('returns true when integration document trigger was configured', async () => { + const { project, commit } = await factories.createProject({ + workspace, + documents: { + 'test.promptl': 'test content', + }, + }) + + const integration = await factories.createIntegration({ + workspace, + type: IntegrationType.Pipedream, + configuration: { + appName: 'slack', + authType: 'oauth', + }, + }) + + await factories.createIntegrationDocumentTrigger({ + workspaceId: workspace.id, + projectId: project.id, + commitId: commit.id, + integrationId: integration.id, + triggerStatus: DocumentTriggerStatus.Deployed, + }) + + const result = await checkNextStepNecessary({ + currentStep: OnboardingStepKey.ConfigureTriggers, + workspace, + }) + + expect(result.ok).toBe(true) + const resultValue = result.unwrap() + expect(resultValue).toBe(true) + }) }) describe('TriggerAgent step', () => { diff --git a/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.ts b/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.ts index 7f69154b84..3f691517cf 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/checkNextStepNecessary.ts @@ -3,11 +3,7 @@ import { Workspace } from '../../../schema/types' import { Result } from '../../../lib/Result' import { DocumentTriggersRepository } from '../../../repositories/documentTriggersRepository' import { IntegrationsRepository } from '../../../repositories/integrationsRepository' -import { - DocumentTriggerStatus, - DocumentTriggerType, - IntegrationType, -} from '@latitude-data/constants' +import { DocumentTriggerType, IntegrationType } from '@latitude-data/constants' import { database } from '../../../client' export async function checkNextStepNecessary( @@ -51,9 +47,7 @@ export async function checkNextStepNecessary( const documentTriggers = documentTriggersResult.unwrap() // For now, only pipedream integrations have triggers const pendingIntegrationTriggers = documentTriggers.filter( - (trigger) => - trigger.triggerType === DocumentTriggerType.Integration && - trigger.triggerStatus === DocumentTriggerStatus.Pending, + (trigger) => trigger.triggerType === DocumentTriggerType.Integration, ) if (pendingIntegrationTriggers.length === 0) { return Result.ok(false) diff --git a/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.test.ts b/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.test.ts index e76115065d..0dd96ca6a6 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.test.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.test.ts @@ -7,6 +7,7 @@ import { DocumentTriggerStatus, IntegrationType, } from '@latitude-data/constants' +import { OnboardingCompleteError } from './onboardingCompleteError' describe('getNextAvailableStep', () => { let workspace: Workspace @@ -110,6 +111,6 @@ describe('getNextAvailableStep', () => { }) expect(result.ok).toBe(false) - expect(() => result.unwrap()).toThrow(new Error('Onboarding is complete')) + expect(() => result.unwrap()).toThrow(OnboardingCompleteError) }) }) diff --git a/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.ts b/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.ts index e4ca26d04d..3a21802589 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/getNextAvailableStep.ts @@ -7,6 +7,7 @@ import { import { checkNextStepNecessary } from './checkNextStepNecessary' import { Workspace } from '../../../schema/types' import { database } from '../../../client' +import { OnboardingCompleteError } from './onboardingCompleteError' export async function getNextAvailableStep( { @@ -21,7 +22,7 @@ export async function getNextAvailableStep( while (true) { const nextStep = getNextStep(currentStep) if (!nextStep) { - return Result.error(new Error('Onboarding is complete')) + return Result.error(new OnboardingCompleteError()) } const checkNextStepNecessaryResult = await checkNextStepNecessary( { diff --git a/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.test.ts b/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.test.ts index 11c21dcedb..e2b788373c 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.test.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.test.ts @@ -10,6 +10,7 @@ import { DocumentTriggerStatus, IntegrationType, } from '@latitude-data/constants' +import { OnboardingCompleteError } from './onboardingCompleteError' describe('moveNextOnboardingStep', () => { let workspaceOnboarding: WorkspaceOnboarding @@ -58,14 +59,10 @@ describe('moveNextOnboardingStep', () => { triggerStatus: DocumentTriggerStatus.Pending, }) - const updatedOnboarding = { - ...workspaceOnboarding, - currentStep: OnboardingStepKey.SetupIntegrations, - } - const result = await moveNextOnboardingStep({ - onboarding: updatedOnboarding, + onboarding: workspaceOnboarding, workspace, + currentStep: OnboardingStepKey.SetupIntegrations, }) expect(result.ok).toBe(true) @@ -74,14 +71,10 @@ describe('moveNextOnboardingStep', () => { }) it('moves from first step directly to third when no pending integration triggers exist', async () => { - const updatedOnboarding = { - ...workspaceOnboarding, - currentStep: OnboardingStepKey.SetupIntegrations, - } - const result = await moveNextOnboardingStep({ - onboarding: updatedOnboarding, + onboarding: workspaceOnboarding, workspace, + currentStep: OnboardingStepKey.SetupIntegrations, }) expect(result.ok).toBe(true) @@ -96,13 +89,9 @@ describe('moveNextOnboardingStep', () => { .set({ currentStep: OnboardingStepKey.ConfigureTriggers }) .where(eq(workspaceOnboardingTable.id, workspaceOnboarding.id)) - const updatedOnboarding = { - ...workspaceOnboarding, - currentStep: OnboardingStepKey.ConfigureTriggers, - } - const result = await moveNextOnboardingStep({ - onboarding: updatedOnboarding, + onboarding: workspaceOnboarding, + currentStep: OnboardingStepKey.ConfigureTriggers, workspace, }) @@ -118,13 +107,9 @@ describe('moveNextOnboardingStep', () => { .set({ currentStep: OnboardingStepKey.TriggerAgent }) .where(eq(workspaceOnboardingTable.id, workspaceOnboarding.id)) - const updatedOnboarding = { - ...workspaceOnboarding, - currentStep: OnboardingStepKey.TriggerAgent, - } - const result = await moveNextOnboardingStep({ - onboarding: updatedOnboarding, + onboarding: workspaceOnboarding, + currentStep: OnboardingStepKey.TriggerAgent, workspace, }) @@ -133,23 +118,6 @@ describe('moveNextOnboardingStep', () => { expect(updated.currentStep).toBe(OnboardingStepKey.RunAgent) }) - it('fails when current step is not set', async () => { - const onboardingWithoutStep = { - ...workspaceOnboarding, - currentStep: null, - } - - const result = await moveNextOnboardingStep({ - onboarding: onboardingWithoutStep, - workspace, - }) - - expect(result.ok).toBe(false) - expect(() => result.unwrap()).toThrow( - new Error('Onboarding current step is not set'), - ) - }) - it('fails when already at the last step', async () => { // Set current step to last step await database @@ -157,17 +125,13 @@ describe('moveNextOnboardingStep', () => { .set({ currentStep: OnboardingStepKey.RunAgent }) .where(eq(workspaceOnboardingTable.id, workspaceOnboarding.id)) - const updatedOnboarding = { - ...workspaceOnboarding, - currentStep: OnboardingStepKey.RunAgent, - } - const result = await moveNextOnboardingStep({ - onboarding: updatedOnboarding, + onboarding: workspaceOnboarding, + currentStep: OnboardingStepKey.RunAgent, workspace, }) expect(result.ok).toBe(false) - expect(() => result.unwrap()).toThrow(new Error('Onboarding is complete')) + expect(() => result.unwrap()).toThrow(OnboardingCompleteError) }) }) diff --git a/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.ts b/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.ts index 69da8792f4..e70b8d99f9 100644 --- a/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.ts +++ b/packages/core/src/services/workspaceOnboarding/steps/moveNextOnboardingStep.ts @@ -4,30 +4,28 @@ import { workspaceOnboarding } from '../../../schema/models/workspaceOnboarding' import { Result } from '../../../lib/Result' import { getNextAvailableStep } from './getNextAvailableStep' import { Workspace, WorkspaceOnboarding } from '../../../schema/types' +import { OnboardingStepKey } from '@latitude-data/constants/onboardingSteps' export async function moveNextOnboardingStep( { onboarding, workspace, + currentStep, }: { onboarding: WorkspaceOnboarding workspace: Workspace + currentStep: OnboardingStepKey }, transaction = new Transaction(), ) { return transaction.call(async (tx) => { - if (!onboarding.currentStep) { - return Result.error(new Error('Onboarding current step is not set')) - } - const getNextAvailableStepResult = await getNextAvailableStep( { - currentStep: onboarding.currentStep, + currentStep, workspace, }, tx, ) - if (!Result.isOk(getNextAvailableStepResult)) { return getNextAvailableStepResult } diff --git a/packages/core/src/services/workspaceOnboarding/steps/onboardingCompleteError.ts b/packages/core/src/services/workspaceOnboarding/steps/onboardingCompleteError.ts new file mode 100644 index 0000000000..028a8d49f3 --- /dev/null +++ b/packages/core/src/services/workspaceOnboarding/steps/onboardingCompleteError.ts @@ -0,0 +1,5 @@ +export class OnboardingCompleteError extends Error { + constructor() { + super('Onboarding is complete') + } +}