mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
Refactor Cody PLG on dotcom (#62035)
* Refactor: extract complex onboarding components * Refactor: extract complex cody/manage components
This commit is contained in:
parent
acb197d4b5
commit
d5c7dfd01b
@ -224,10 +224,16 @@ ts_project(
|
||||
"src/cody/components/ScopeSelector/useRepoSuggestions.ts",
|
||||
"src/cody/dashboard/CodyDashboardPage.tsx",
|
||||
"src/cody/dashboard/UpsellImage.tsx",
|
||||
"src/cody/editorGroups.ts",
|
||||
"src/cody/featureFlags.ts",
|
||||
"src/cody/isCodyEnabled.tsx",
|
||||
"src/cody/management/CodyManagementPage.tsx",
|
||||
"src/cody/management/SubscriptionStats.tsx",
|
||||
"src/cody/management/UseCodyInEditorSection.tsx",
|
||||
"src/cody/onboarding/CodyOnboarding.tsx",
|
||||
"src/cody/onboarding/EditorStep.tsx",
|
||||
"src/cody/onboarding/PurposeStep.tsx",
|
||||
"src/cody/onboarding/WelcomeStep.tsx",
|
||||
"src/cody/onboarding/instructions/CodyFeatures.tsx",
|
||||
"src/cody/onboarding/instructions/JetBrains.tsx",
|
||||
"src/cody/onboarding/instructions/NeoVim.tsx",
|
||||
|
||||
94
client/web/src/cody/editorGroups.ts
Normal file
94
client/web/src/cody/editorGroups.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import type { IEditor } from './onboarding/CodyOnboarding'
|
||||
import { JetBrainsInstructions } from './onboarding/instructions/JetBrains'
|
||||
import { NeoVimInstructions } from './onboarding/instructions/NeoVim'
|
||||
import { VSCodeInstructions } from './onboarding/instructions/VsCode'
|
||||
|
||||
export const editorGroups: IEditor[][] = [
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
icon: 'VsCode',
|
||||
name: 'VS Code',
|
||||
publisher: 'Microsoft',
|
||||
releaseStage: 'Stable',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-vscode',
|
||||
instructions: VSCodeInstructions,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'IntelliJ',
|
||||
name: 'IntelliJ IDEA',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'PhpStorm',
|
||||
name: 'PhpStorm ',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'PyCharm',
|
||||
name: 'PyCharm',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 5,
|
||||
icon: 'WebStorm',
|
||||
name: 'WebStorm',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: 'RubyMine',
|
||||
name: 'RubyMine',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
icon: 'GoLand',
|
||||
name: 'GoLand',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
icon: 'AndroidStudio',
|
||||
name: 'Android Studio',
|
||||
publisher: 'Google',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 9,
|
||||
icon: 'NeoVim',
|
||||
name: 'Neovim',
|
||||
publisher: 'Neovim Team',
|
||||
releaseStage: 'Experimental',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-neovim',
|
||||
instructions: NeoVimInstructions,
|
||||
},
|
||||
],
|
||||
]
|
||||
@ -1,45 +1,32 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import { mdiHelpCircleOutline, mdiInformationOutline, mdiOpenInNew, mdiCreditCardOutline } from '@mdi/js'
|
||||
import { mdiCreditCardOutline } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
|
||||
import { useQuery } from '@sourcegraph/http-client'
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import {
|
||||
ButtonLink,
|
||||
H1,
|
||||
H2,
|
||||
H4,
|
||||
H5,
|
||||
Icon,
|
||||
Link,
|
||||
LoadingSpinner,
|
||||
Modal,
|
||||
PageHeader,
|
||||
Text,
|
||||
useSearchParameters,
|
||||
} from '@sourcegraph/wildcard'
|
||||
import { ButtonLink, H1, H2, Icon, Link, PageHeader, Text, useSearchParameters } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { AuthenticatedUser } from '../../auth'
|
||||
import { Page } from '../../components/Page'
|
||||
import { PageTitle } from '../../components/PageTitle'
|
||||
import type {
|
||||
UserCodyPlanResult,
|
||||
UserCodyPlanVariables,
|
||||
UserCodyUsageResult,
|
||||
UserCodyUsageVariables,
|
||||
import {
|
||||
type UserCodyPlanResult,
|
||||
type UserCodyPlanVariables,
|
||||
type UserCodyUsageResult,
|
||||
type UserCodyUsageVariables,
|
||||
CodySubscriptionPlan,
|
||||
} from '../../graphql-operations'
|
||||
import { CodySubscriptionPlan } from '../../graphql-operations'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { EventName } from '../../util/constants'
|
||||
import { CodyProIcon, AutocompletesIcon, ChatMessagesIcon, DashboardIcon } from '../components/CodyIcon'
|
||||
import { CodyProIcon, DashboardIcon } from '../components/CodyIcon'
|
||||
import { isCodyEnabled } from '../isCodyEnabled'
|
||||
import { CodyOnboarding, editorGroups, type IEditor } from '../onboarding/CodyOnboarding'
|
||||
import { ProTierIcon, useCodyPaymentsUrl } from '../subscription/CodySubscriptionPage'
|
||||
import { CodyOnboarding, type IEditor } from '../onboarding/CodyOnboarding'
|
||||
import { useCodyPaymentsUrl } from '../subscription/CodySubscriptionPage'
|
||||
import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries'
|
||||
|
||||
import { SubscriptionStats } from './SubscriptionStats'
|
||||
import { UseCodyInEditorSection } from './UseCodyInEditorSection'
|
||||
|
||||
import styles from './CodyManagementPage.module.scss'
|
||||
|
||||
interface CodyManagementPageProps extends TelemetryV2Props {
|
||||
@ -62,7 +49,6 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
const utm_source = parameters.get('utm_source')
|
||||
|
||||
useEffect(() => {
|
||||
eventLogger.log(EventName.CODY_MANAGEMENT_PAGE_VIEWED, { utm_source })
|
||||
telemetryRecorder.recordEvent('cody.management', 'view')
|
||||
}, [utm_source, telemetryRecorder])
|
||||
|
||||
@ -73,12 +59,6 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
{}
|
||||
)
|
||||
|
||||
const stats = usageData?.currentUser
|
||||
const codyCurrentPeriodChatLimit = stats?.codyCurrentPeriodChatLimit || 0
|
||||
const codyCurrentPeriodChatUsage = stats?.codyCurrentPeriodChatUsage || 0
|
||||
const codyCurrentPeriodCodeLimit = stats?.codyCurrentPeriodCodeLimit || 0
|
||||
const codyCurrentPeriodCodeUsage = stats?.codyCurrentPeriodCodeUsage || 0
|
||||
|
||||
const [selectedEditor, setSelectedEditor] = React.useState<IEditor | null>(null)
|
||||
const [selectedEditorStep, setSelectedEditorStep] = React.useState<EditorStep | null>(null)
|
||||
|
||||
@ -107,29 +87,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
return null
|
||||
}
|
||||
|
||||
const codeLimitReached = codyCurrentPeriodCodeUsage >= codyCurrentPeriodCodeLimit && codyCurrentPeriodCodeLimit > 0
|
||||
const chatLimitReached = codyCurrentPeriodChatUsage >= codyCurrentPeriodChatLimit && codyCurrentPeriodChatLimit > 0
|
||||
const userIsOnProTier = subscription.plan === CodySubscriptionPlan.PRO
|
||||
|
||||
// Flag usage limits as resetting based on the current subscription's billing cycle.
|
||||
//
|
||||
// BUG: The usage limit refresh should be independent of a user's subscription data.
|
||||
// e.g. if we offered an annual billing plan, we'd want to reset usage more often.
|
||||
// sourcegraph#59990 is related, and required for the times to line up with the
|
||||
// behavior from Cody Gateway.
|
||||
//
|
||||
// BUG: If the subscription is canceled, this will be in the past and therefore invalid.
|
||||
// This data should be fetched from the SSC backend, and like above, separeate
|
||||
// from the user's subscription billing cycle.
|
||||
const usageRefreshTime = subscription.currentPeriodEndAt
|
||||
|
||||
// Time when the user's current subscription will end.
|
||||
//
|
||||
// BUG: If the subscription is in the canceled state, this will be in the past. We need
|
||||
// to update the UI to simply say "subscription canceled" or "you are on the free"
|
||||
// plan, you don't have any subscription billing cycle anchors".
|
||||
//
|
||||
const codyProSubscriptionEndTime = subscription.currentPeriodEndAt
|
||||
const isUserOnProTier = subscription.plan === CodySubscriptionPlan.PRO
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -143,14 +101,14 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
|
||||
{!userIsOnProTier && <UpgradeToProBanner onClick={onClickUpgradeToProCTA} />}
|
||||
{!isUserOnProTier && <UpgradeToProBanner onClick={onClickUpgradeToProCTA} />}
|
||||
|
||||
<div className={classNames('p-4 border bg-1 mt-4', styles.container)}>
|
||||
<div className="d-flex justify-content-between align-items-center border-bottom pb-3">
|
||||
<div>
|
||||
<H2>My subscription</H2>
|
||||
<Text className="text-muted mb-0">
|
||||
{userIsOnProTier ? (
|
||||
{isUserOnProTier ? (
|
||||
'You are on the Pro tier.'
|
||||
) : (
|
||||
<span>
|
||||
@ -160,7 +118,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
{userIsOnProTier && (
|
||||
{isUserOnProTier && (
|
||||
<div>
|
||||
<ButtonLink
|
||||
variant="primary"
|
||||
@ -168,7 +126,6 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
href={manageSubscriptionRedirectURL}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
eventLogger.log(EventName.CODY_MANAGE_SUBSCRIPTION_CLICKED)
|
||||
telemetryRecorder.recordEvent('cody.manageSubscription', 'click')
|
||||
window.location.href = manageSubscriptionRedirectURL
|
||||
}}
|
||||
@ -179,242 +136,19 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames('d-flex align-items-center mt-3', styles.responsiveContainer)}>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
|
||||
{userIsOnProTier ? (
|
||||
<ProTierIcon />
|
||||
) : (
|
||||
<Text className={classNames(styles.planName, 'mb-0')}>Free</Text>
|
||||
)}
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
tier
|
||||
</Text>
|
||||
{userIsOnProTier && subscription.cancelAtPeriodEnd && (
|
||||
<Text className="text-muted mb-0 mt-4" size="small">
|
||||
Subscription ends <Timestamp date={codyProSubscriptionEndTime} />
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3 border-left border-right">
|
||||
<AutocompletesIcon />
|
||||
<div className="mb-2 mt-3">
|
||||
{subscription.applyProRateLimits ? (
|
||||
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
|
||||
Unlimited
|
||||
</Text>
|
||||
) : usageData?.currentUser ? (
|
||||
<>
|
||||
<Text
|
||||
weight="bold"
|
||||
className={classNames(
|
||||
'd-inline mb-0',
|
||||
styles.counter,
|
||||
codeLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
>
|
||||
{Math.min(codyCurrentPeriodCodeUsage, codyCurrentPeriodCodeLimit)} /
|
||||
</Text>{' '}
|
||||
<Text
|
||||
className={classNames(
|
||||
'd-inline b-0',
|
||||
codeLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
size="small"
|
||||
>
|
||||
{codyCurrentPeriodCodeLimit}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</div>
|
||||
<H4 className={classNames('mb-0', codeLimitReached ? 'text-danger' : 'text-muted')}>
|
||||
Autocomplete suggestions
|
||||
</H4>
|
||||
{!subscription.applyProRateLimits &&
|
||||
(codeLimitReached ? (
|
||||
<Text className="text-danger mb-0" size="small">
|
||||
Renews in <Timestamp date={usageRefreshTime} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
this month
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
|
||||
<ChatMessagesIcon />
|
||||
<div className="mb-2 mt-3">
|
||||
{subscription.applyProRateLimits ? (
|
||||
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
|
||||
Unlimited
|
||||
</Text>
|
||||
) : usageData?.currentUser ? (
|
||||
<>
|
||||
<Text
|
||||
weight="bold"
|
||||
className={classNames(
|
||||
'd-inline mb-0',
|
||||
styles.counter,
|
||||
chatLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
>
|
||||
{Math.min(codyCurrentPeriodChatUsage, codyCurrentPeriodChatLimit)} /
|
||||
</Text>{' '}
|
||||
<Text
|
||||
className={classNames(
|
||||
'd-inline b-0',
|
||||
chatLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
size="small"
|
||||
>
|
||||
{codyCurrentPeriodChatLimit}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</div>
|
||||
<H4 className={classNames('mb-0', chatLimitReached ? 'text-danger' : 'text-muted')}>
|
||||
Chat messages and commands
|
||||
</H4>
|
||||
{!subscription.applyProRateLimits &&
|
||||
(chatLimitReached && subscription.currentPeriodEndAt ? (
|
||||
<Text className="text-danger mb-0" size="small">
|
||||
Renews <Timestamp date={usageRefreshTime} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
this month
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<SubscriptionStats {...{ subscription, usageData }} />
|
||||
</div>
|
||||
|
||||
<div className={classNames('p-4 border bg-1 mt-4 mb-5', styles.container)}>
|
||||
<div className="d-flex justify-content-between align-items-center border-bottom pb-3">
|
||||
<div>
|
||||
<H2>Use Cody directly in your editor</H2>
|
||||
<Text className="text-muted mb-0">
|
||||
Download the Cody extension in your editor to start using Cody.
|
||||
</Text>
|
||||
</div>
|
||||
{userIsOnProTier ? (
|
||||
<div>
|
||||
<Link
|
||||
to="https://help.sourcegraph.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted text-sm"
|
||||
>
|
||||
<Icon svgPath={mdiHelpCircleOutline} className="mr-1" aria-hidden={true} />
|
||||
Join our community, read our docs, or get product/billing support
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{editorGroups.map((group, index) => (
|
||||
<div
|
||||
key={group.map(editor => editor.name).join('-')}
|
||||
className={classNames('d-flex mt-3', styles.responsiveContainer, {
|
||||
'border-bottom pb-3': index < group.length - 1,
|
||||
})}
|
||||
>
|
||||
{group.map((editor, index) => (
|
||||
<div
|
||||
key={editor.name}
|
||||
className={classNames('d-flex flex-column flex-1 pt-3 px-3', {
|
||||
'border-left': index !== 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames('d-flex mb-3 align-items-center', styles.ideHeader)}
|
||||
onClick={() => {
|
||||
setSelectedEditor(editor)
|
||||
setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSelectedEditor(editor)
|
||||
setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
alt={editor.name}
|
||||
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
|
||||
width={34}
|
||||
className="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
{editor.publisher}
|
||||
</Text>
|
||||
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
|
||||
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editor.instructions && (
|
||||
<Link
|
||||
to="#"
|
||||
className="mb-2 text-muted d-flex align-items-center"
|
||||
onClick={() => {
|
||||
setSelectedEditor(editor)
|
||||
setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiInformationOutline} aria-hidden={true} className="mr-1" />{' '}
|
||||
Quickstart guide
|
||||
</Link>
|
||||
)}
|
||||
{editor.docs && (
|
||||
<Link
|
||||
to={editor.docs}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-muted d-flex align-items-center"
|
||||
>
|
||||
<Icon svgPath={mdiOpenInNew} aria-hidden={true} className="mr-1" />{' '}
|
||||
Documentation
|
||||
</Link>
|
||||
)}
|
||||
{selectedEditor?.name === editor.name &&
|
||||
selectedEditorStep !== null &&
|
||||
editor.instructions && (
|
||||
<Modal
|
||||
key={editor.name + '-modal'}
|
||||
isOpen={true}
|
||||
aria-label={`${editor.name} Info`}
|
||||
className={styles.modal}
|
||||
position="center"
|
||||
>
|
||||
<editor.instructions
|
||||
showStep={selectedEditorStep}
|
||||
onClose={() => {
|
||||
setSelectedEditor(null)
|
||||
setSelectedEditorStep(null)
|
||||
}}
|
||||
telemetryRecorder={telemetryRecorder}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{group.length < 4
|
||||
? [...new Array(4 - group.length)].map((_, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} className="flex-1 p-3" />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<UseCodyInEditorSection
|
||||
{...{
|
||||
selectedEditor,
|
||||
setSelectedEditor,
|
||||
selectedEditorStep,
|
||||
setSelectedEditorStep,
|
||||
isUserOnProTier,
|
||||
telemetryRecorder,
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
<CodyOnboarding authenticatedUser={authenticatedUser} telemetryRecorder={telemetryRecorder} />
|
||||
</>
|
||||
|
||||
164
client/web/src/cody/management/SubscriptionStats.tsx
Normal file
164
client/web/src/cody/management/SubscriptionStats.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
|
||||
import { type CodySubscriptionStatus, CodySubscriptionPlan } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { Text, LoadingSpinner, H4 } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { UserCodyUsageResult } from '../../graphql-operations'
|
||||
import { AutocompletesIcon, ChatMessagesIcon } from '../components/CodyIcon'
|
||||
import { ProTierIcon } from '../subscription/CodySubscriptionPage'
|
||||
|
||||
import styles from './CodyManagementPage.module.scss'
|
||||
|
||||
interface SubscriptionStatsProps {
|
||||
subscription: {
|
||||
status: CodySubscriptionStatus
|
||||
plan: CodySubscriptionPlan
|
||||
applyProRateLimits: boolean
|
||||
currentPeriodStartAt: string
|
||||
currentPeriodEndAt: string
|
||||
cancelAtPeriodEnd: boolean
|
||||
}
|
||||
usageData: UserCodyUsageResult | undefined
|
||||
}
|
||||
|
||||
export const SubscriptionStats: React.FunctionComponent<SubscriptionStatsProps> = ({
|
||||
subscription,
|
||||
usageData,
|
||||
}: SubscriptionStatsProps) => {
|
||||
const stats = usageData?.currentUser
|
||||
const codyCurrentPeriodChatLimit = stats?.codyCurrentPeriodChatLimit || 0
|
||||
const codyCurrentPeriodChatUsage = stats?.codyCurrentPeriodChatUsage || 0
|
||||
const codyCurrentPeriodCodeLimit = stats?.codyCurrentPeriodCodeLimit || 0
|
||||
const codyCurrentPeriodCodeUsage = stats?.codyCurrentPeriodCodeUsage || 0
|
||||
|
||||
const codeLimitReached = codyCurrentPeriodCodeUsage >= codyCurrentPeriodCodeLimit && codyCurrentPeriodCodeLimit > 0
|
||||
const chatLimitReached = codyCurrentPeriodChatUsage >= codyCurrentPeriodChatLimit && codyCurrentPeriodChatLimit > 0
|
||||
const isUserOnProTier = subscription.plan === CodySubscriptionPlan.PRO
|
||||
|
||||
// Flag usage limits as resetting based on the current subscription's billing cycle.
|
||||
//
|
||||
// BUG: The usage limit refresh should be independent of a user's subscription data.
|
||||
// e.g. if we offered an annual billing plan, we'd want to reset usage more often.
|
||||
// sourcegraph#59990 is related, and required for the times to line up with the
|
||||
// behavior from Cody Gateway.
|
||||
//
|
||||
// BUG: If the subscription is canceled, this will be in the past and therefore invalid.
|
||||
// This data should be fetched from the SSC backend, and like above, separate
|
||||
// from the user's subscription billing cycle.
|
||||
const usageRefreshTime = subscription.currentPeriodEndAt
|
||||
|
||||
// Time when the user's current subscription will end.
|
||||
//
|
||||
// BUG: If the subscription is in the canceled state, this will be in the past. We need
|
||||
// to update the UI to simply say "subscription canceled" or "you are on the free"
|
||||
// plan, you don't have any subscription billing cycle anchors".
|
||||
//
|
||||
const codyProSubscriptionEndTime = subscription.currentPeriodEndAt
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex align-items-center mt-3', styles.responsiveContainer)}>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
|
||||
{isUserOnProTier ? <ProTierIcon /> : <Text className={classNames(styles.planName, 'mb-0')}>Free</Text>}
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
tier
|
||||
</Text>
|
||||
{isUserOnProTier && subscription.cancelAtPeriodEnd && (
|
||||
<Text className="text-muted mb-0 mt-4" size="small">
|
||||
Subscription ends <Timestamp date={codyProSubscriptionEndTime} />
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3 border-left border-right">
|
||||
<AutocompletesIcon />
|
||||
<div className="mb-2 mt-3">
|
||||
{subscription.applyProRateLimits ? (
|
||||
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
|
||||
Unlimited
|
||||
</Text>
|
||||
) : usageData?.currentUser ? (
|
||||
<>
|
||||
<Text
|
||||
weight="bold"
|
||||
className={classNames(
|
||||
'd-inline mb-0',
|
||||
styles.counter,
|
||||
codeLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
>
|
||||
{Math.min(codyCurrentPeriodCodeUsage, codyCurrentPeriodCodeLimit)} /
|
||||
</Text>{' '}
|
||||
<Text
|
||||
className={classNames('d-inline b-0', codeLimitReached ? 'text-danger' : 'text-muted')}
|
||||
size="small"
|
||||
>
|
||||
{codyCurrentPeriodCodeLimit}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</div>
|
||||
<H4 className={classNames('mb-0', codeLimitReached ? 'text-danger' : 'text-muted')}>
|
||||
Autocomplete suggestions
|
||||
</H4>
|
||||
{!subscription.applyProRateLimits &&
|
||||
(codeLimitReached ? (
|
||||
<Text className="text-danger mb-0" size="small">
|
||||
Renews in <Timestamp date={usageRefreshTime} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
this month
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
|
||||
<ChatMessagesIcon />
|
||||
<div className="mb-2 mt-3">
|
||||
{subscription.applyProRateLimits ? (
|
||||
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
|
||||
Unlimited
|
||||
</Text>
|
||||
) : usageData?.currentUser ? (
|
||||
<>
|
||||
<Text
|
||||
weight="bold"
|
||||
className={classNames(
|
||||
'd-inline mb-0',
|
||||
styles.counter,
|
||||
chatLimitReached ? 'text-danger' : 'text-muted'
|
||||
)}
|
||||
>
|
||||
{Math.min(codyCurrentPeriodChatUsage, codyCurrentPeriodChatLimit)} /
|
||||
</Text>{' '}
|
||||
<Text
|
||||
className={classNames('d-inline b-0', chatLimitReached ? 'text-danger' : 'text-muted')}
|
||||
size="small"
|
||||
>
|
||||
{codyCurrentPeriodChatLimit}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</div>
|
||||
<H4 className={classNames('mb-0', chatLimitReached ? 'text-danger' : 'text-muted')}>
|
||||
Chat messages and commands
|
||||
</H4>
|
||||
{!subscription.applyProRateLimits &&
|
||||
(chatLimitReached && usageRefreshTime ? (
|
||||
<Text className="text-danger mb-0" size="small">
|
||||
Renews <Timestamp date={usageRefreshTime} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
this month
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
145
client/web/src/cody/management/UseCodyInEditorSection.tsx
Normal file
145
client/web/src/cody/management/UseCodyInEditorSection.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React from 'react'
|
||||
|
||||
import { mdiHelpCircleOutline, mdiInformationOutline, mdiOpenInNew } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import { H2, Text, Link, Icon, H5, Modal } from '@sourcegraph/wildcard'
|
||||
|
||||
import { editorGroups } from '../editorGroups'
|
||||
import type { IEditor } from '../onboarding/CodyOnboarding'
|
||||
|
||||
import { EditorStep } from './CodyManagementPage'
|
||||
|
||||
import styles from './CodyManagementPage.module.scss'
|
||||
|
||||
interface UseCodyInEditorSectionProps extends TelemetryV2Props {
|
||||
selectedEditor: IEditor | null
|
||||
setSelectedEditor: (editor: IEditor | null) => void
|
||||
selectedEditorStep: EditorStep | null
|
||||
setSelectedEditorStep: (step: EditorStep | null) => void
|
||||
isUserOnProTier: boolean
|
||||
}
|
||||
|
||||
export const UseCodyInEditorSection: React.FunctionComponent<UseCodyInEditorSectionProps> = props => (
|
||||
<div className={classNames('p-4 border bg-1 mt-4 mb-5', styles.container)}>
|
||||
<div className="d-flex justify-content-between align-items-center border-bottom pb-3">
|
||||
<div>
|
||||
<H2>Use Cody directly in your editor</H2>
|
||||
<Text className="text-muted mb-0">Download the Cody extension in your editor to start using Cody.</Text>
|
||||
</div>
|
||||
{props.isUserOnProTier ? (
|
||||
<div>
|
||||
<Link
|
||||
to="https://help.sourcegraph.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted text-sm"
|
||||
>
|
||||
<Icon svgPath={mdiHelpCircleOutline} className="mr-1" aria-hidden={true} />
|
||||
Join our community, read our docs, or get product/billing support
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{editorGroups.map((group, index) => (
|
||||
<div
|
||||
key={group.map(editor => editor.name).join('-')}
|
||||
className={classNames('d-flex mt-3', styles.responsiveContainer, {
|
||||
'border-bottom pb-3': index < group.length - 1,
|
||||
})}
|
||||
>
|
||||
{group.map((editor, index) => (
|
||||
<div
|
||||
key={editor.name}
|
||||
className={classNames('d-flex flex-column flex-1 pt-3 px-3', {
|
||||
'border-left': index !== 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames('d-flex mb-3 align-items-center', styles.ideHeader)}
|
||||
onClick={() => {
|
||||
props.setSelectedEditor(editor)
|
||||
props.setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
props.setSelectedEditor(editor)
|
||||
props.setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
alt={editor.name}
|
||||
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
|
||||
width={34}
|
||||
className="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-muted mb-0" size="small">
|
||||
{editor.publisher}
|
||||
</Text>
|
||||
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
|
||||
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editor.instructions && (
|
||||
<Link
|
||||
to="#"
|
||||
className="mb-2 text-muted d-flex align-items-center"
|
||||
onClick={() => {
|
||||
props.setSelectedEditor(editor)
|
||||
props.setSelectedEditorStep(EditorStep.SetupInstructions)
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiInformationOutline} aria-hidden={true} className="mr-1" /> Quickstart
|
||||
guide
|
||||
</Link>
|
||||
)}
|
||||
{editor.docs && (
|
||||
<Link
|
||||
to={editor.docs}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-muted d-flex align-items-center"
|
||||
>
|
||||
<Icon svgPath={mdiOpenInNew} aria-hidden={true} className="mr-1" /> Documentation
|
||||
</Link>
|
||||
)}
|
||||
{props.selectedEditor?.name === editor.name &&
|
||||
props.selectedEditorStep !== null &&
|
||||
editor.instructions && (
|
||||
<Modal
|
||||
key={editor.name + '-modal'}
|
||||
isOpen={true}
|
||||
aria-label={`${editor.name} Info`}
|
||||
className={styles.modal}
|
||||
position="center"
|
||||
>
|
||||
<editor.instructions
|
||||
showStep={props.selectedEditorStep}
|
||||
onClose={() => {
|
||||
props.setSelectedEditor(null)
|
||||
props.setSelectedEditorStep(null)
|
||||
}}
|
||||
telemetryRecorder={props.telemetryRecorder}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{group.length < 4
|
||||
? [...new Array(4 - group.length)].map((_, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} className="flex-1 p-3" />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@ -1,22 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
|
||||
import type { TelemetryRecorder, TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { Button, H2, H5, Modal, Text, useSearchParameters } from '@sourcegraph/wildcard'
|
||||
import { Modal, useSearchParameters } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { AuthenticatedUser } from '../../auth'
|
||||
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
|
||||
import { HubSpotForm } from '../../marketing/components/HubSpotForm'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { EventName } from '../../util/constants'
|
||||
|
||||
import { JetBrainsInstructions } from './instructions/JetBrains'
|
||||
import { NeoVimInstructions } from './instructions/NeoVim'
|
||||
import { VSCodeInstructions } from './instructions/VsCode'
|
||||
import { EditorStep } from './EditorStep'
|
||||
import { PurposeStep } from './PurposeStep'
|
||||
import { WelcomeStep } from './WelcomeStep'
|
||||
|
||||
import styles from './CodyOnboarding.module.scss'
|
||||
|
||||
@ -35,96 +30,6 @@ export interface IEditor {
|
||||
}>
|
||||
}
|
||||
|
||||
export const editorGroups: IEditor[][] = [
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
icon: 'VsCode',
|
||||
name: 'VS Code',
|
||||
publisher: 'Microsoft',
|
||||
releaseStage: 'Stable',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-vscode',
|
||||
instructions: VSCodeInstructions,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'IntelliJ',
|
||||
name: 'IntelliJ IDEA',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'PhpStorm',
|
||||
name: 'PhpStorm ',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'PyCharm',
|
||||
name: 'PyCharm',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 5,
|
||||
icon: 'WebStorm',
|
||||
name: 'WebStorm',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: 'RubyMine',
|
||||
name: 'RubyMine',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
icon: 'GoLand',
|
||||
name: 'GoLand',
|
||||
publisher: 'JetBrains',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
icon: 'AndroidStudio',
|
||||
name: 'Android Studio',
|
||||
publisher: 'Google',
|
||||
releaseStage: 'Beta',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
|
||||
instructions: JetBrainsInstructions,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 9,
|
||||
icon: 'NeoVim',
|
||||
name: 'Neovim',
|
||||
publisher: 'Neovim Team',
|
||||
releaseStage: 'Experimental',
|
||||
docs: 'https://sourcegraph.com/docs/cody/clients/install-neovim',
|
||||
instructions: NeoVimInstructions,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
interface CodyOnboardingProps extends TelemetryV2Props {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
@ -178,8 +83,9 @@ export function CodyOnboarding({ authenticatedUser, telemetryRecorder }: CodyOnb
|
||||
}
|
||||
setOnboardingStep(currentsStep => (currentsStep || 0) + 2)
|
||||
handleShowLastStep()
|
||||
const metadata = { variant: 'control' }
|
||||
eventLogger.log(EventName.CODY_HANDRAISER_TEST_ENROLLMENT, metadata, metadata)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.hubspotForm.fromWorkPersonalToHandRaiserTest', 'enroll', {
|
||||
metadata: { controlVariant: 1 },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -216,266 +122,3 @@ export function CodyOnboarding({ authenticatedUser, telemetryRecorder }: CodyOnb
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function WelcomeStep({
|
||||
onNext,
|
||||
pro,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onNext: () => void
|
||||
pro: boolean
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
const [show, setShow] = useState(false)
|
||||
const isLightTheme = useIsLightTheme()
|
||||
useEffect(() => {
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_WELCOME_VIEWED,
|
||||
{ tier: pro ? 'pro' : 'free' },
|
||||
{ tier: pro ? 'pro' : 'free' }
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.welcome', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
useEffect(() => {
|
||||
// theme is not ready on first render, it defaults to system theme.
|
||||
// so we need to wait a bit before showing the welcome video.
|
||||
setTimeout(() => {
|
||||
setShow(true)
|
||||
}, 500)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex flex-column align-items-center p-5')}>
|
||||
{show ? (
|
||||
<>
|
||||
<video width="180" className={classNames('mb-5', styles.welcomeVideo)} autoPlay={true} muted={true}>
|
||||
<source
|
||||
src={
|
||||
isLightTheme
|
||||
? 'https://storage.googleapis.com/sourcegraph-assets/hiCodyWhite.mp4'
|
||||
: 'https://storage.googleapis.com/sourcegraph-assets/hiCodyDark.mp4'
|
||||
}
|
||||
type="video/mp4"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<Text className={classNames('mb-4 pb-4', styles.fadeIn, styles.fadeSecond, styles.welcomeSubtitle)}>
|
||||
Ready to breeze through the basics and get comfortable with Cody
|
||||
{pro ? ' Pro' : ''}?
|
||||
</Text>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className={classNames(styles.fadeIn, styles.fadeThird)}
|
||||
>
|
||||
Sure, let's dive in!
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.blankPlaceholder} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PurposeStep({
|
||||
onNext,
|
||||
pro,
|
||||
authenticatedUser,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onNext: () => void
|
||||
pro: boolean
|
||||
authenticatedUser: AuthenticatedUser
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
useEffect(() => {
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_PURPOSE_VIEWED,
|
||||
{ tier: pro ? 'pro' : 'free' },
|
||||
{ tier: pro ? 'pro' : 'free' }
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
const primaryEmail = authenticatedUser.emails.find(email => email.isPrimary)?.email
|
||||
|
||||
const handleFormSubmit = (form: HTMLFormElement): void => {
|
||||
const choice = form[0].querySelector<HTMLInputElement>('input[name="cody_form_hand_raiser"]')
|
||||
|
||||
if (choice) {
|
||||
const metadata = { variant: 'treatment' }
|
||||
|
||||
eventLogger.log(EventName.CODY_HANDRAISER_TEST_ENROLLMENT, metadata, metadata)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'select', {
|
||||
metadata: { onboardingCall: choice.checked ? 1 : 0 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-bottom pb-3 mb-3">
|
||||
<H2 className="mb-1">Would you like to learn more about Cody for enterprise?</H2>
|
||||
<Text className="mb-0 text-muted" size="small">
|
||||
If you're not ready for a conversation, we'll stick to sharing Cody onboarding resources.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="d-flex align-items-center border-bottom mb-3 pb-3 justify-content-center">
|
||||
<HubSpotForm
|
||||
formId="19f34edd-1a98-4fc9-9b2b-c1edca727720"
|
||||
onFormSubmitted={() => {
|
||||
onNext()
|
||||
}}
|
||||
onFormLoadError={() => {
|
||||
onNext()
|
||||
}}
|
||||
userId={authenticatedUser.id}
|
||||
userEmail={primaryEmail}
|
||||
masterFormName="qualificationSurvey"
|
||||
onFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function EditorStep({
|
||||
onCompleted,
|
||||
pro,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onCompleted: () => void
|
||||
pro: boolean
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
useEffect(() => {
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_CHOOSE_EDITOR_VIEWED,
|
||||
{ tier: pro ? 'pro' : 'free' },
|
||||
{ tier: pro ? 'pro' : 'free' }
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
const [editor, setEditor] = useState<null | IEditor>(null)
|
||||
|
||||
const onBack = (): void => setEditor(null)
|
||||
|
||||
if (editor?.instructions) {
|
||||
const Instructions = editor.instructions
|
||||
|
||||
return <Instructions onBack={onBack} onClose={onCompleted} telemetryRecorder={telemetryRecorder} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-bottom pb-3 mb-3">
|
||||
<H2 className="mb-1">Choose your editor</H2>
|
||||
<Text className="mb-0 text-muted" size="small">
|
||||
Most of Cody experience happens in the IDE. Let's get that set up.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="mb-3 border-bottom pb-3">
|
||||
{editorGroups.map((group, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames('d-flex mt-3', styles.responsiveContainer, {
|
||||
'border-bottom pb-3': index < group.length - 1,
|
||||
})}
|
||||
>
|
||||
{group.map((editor, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames('d-flex flex-column flex-1 p-3 cursor-pointer', styles.ideGrid, {
|
||||
'border-left': index !== 0,
|
||||
})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={() => {
|
||||
setEditor(editor)
|
||||
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_CHOOSE_EDITOR_SELECTED,
|
||||
{
|
||||
tier: pro ? 'pro' : 'free',
|
||||
editor,
|
||||
},
|
||||
{
|
||||
tier: pro ? 'pro' : 'free',
|
||||
editor,
|
||||
}
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
|
||||
metadata: { tier: pro ? 1 : 0, editor: editor.id },
|
||||
})
|
||||
}}
|
||||
onClick={() => {
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_CHOOSE_EDITOR_SELECTED,
|
||||
{
|
||||
tier: pro ? 'pro' : 'free',
|
||||
editor,
|
||||
},
|
||||
{
|
||||
tier: pro ? 'pro' : 'free',
|
||||
editor,
|
||||
}
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
|
||||
metadata: { tier: pro ? 1 : 0, editor: editor.id },
|
||||
})
|
||||
setEditor(editor)
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div>
|
||||
<img
|
||||
alt={editor.name}
|
||||
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
|
||||
width={34}
|
||||
className="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-muted mb-0 text-truncate" size="small">
|
||||
{editor.publisher}
|
||||
</Text>
|
||||
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
|
||||
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{group.length < 4
|
||||
? [...new Array(4 - group.length)].map((_, index) => (
|
||||
<div key={index} className="flex-1 p-3" />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="d-flex justify-content-end align-items-center">
|
||||
<Text
|
||||
className="mb-0 text-muted cursor-pointer"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onCompleted()
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_CHOOSE_EDITOR_SKIPPED,
|
||||
{ tier: pro ? 'pro' : 'free' },
|
||||
{ tier: pro ? 'pro' : 'free' }
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'skip', {
|
||||
metadata: { tier: pro ? 1 : 0 },
|
||||
})
|
||||
}}
|
||||
>
|
||||
Skip for now
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
118
client/web/src/cody/onboarding/EditorStep.tsx
Normal file
118
client/web/src/cody/onboarding/EditorStep.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
|
||||
import { H2, Text, H5 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { editorGroups } from '../editorGroups'
|
||||
|
||||
import type { IEditor } from './CodyOnboarding'
|
||||
|
||||
import styles from './CodyOnboarding.module.scss'
|
||||
|
||||
export function EditorStep({
|
||||
onCompleted,
|
||||
pro,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onCompleted: () => void
|
||||
pro: boolean
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
useEffect(() => {
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
const [editor, setEditor] = useState<null | IEditor>(null)
|
||||
|
||||
const onBack = (): void => setEditor(null)
|
||||
|
||||
if (editor?.instructions) {
|
||||
const Instructions = editor.instructions
|
||||
|
||||
return <Instructions onBack={onBack} onClose={onCompleted} telemetryRecorder={telemetryRecorder} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-bottom pb-3 mb-3">
|
||||
<H2 className="mb-1">Choose your editor</H2>
|
||||
<Text className="mb-0 text-muted" size="small">
|
||||
Most of Cody experience happens in the IDE. Let's get that set up.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="mb-3 border-bottom pb-3">
|
||||
{editorGroups.map((group, groupIndex) => (
|
||||
<div
|
||||
key={group[0].id}
|
||||
className={classNames('d-flex mt-3', styles.responsiveContainer, {
|
||||
'border-bottom pb-3': groupIndex < group.length - 1,
|
||||
})}
|
||||
>
|
||||
{group.map((editor, editorIndex) => (
|
||||
<div
|
||||
key={editor.id}
|
||||
className={classNames('d-flex flex-column flex-1 p-3 cursor-pointer', styles.ideGrid, {
|
||||
'border-left': editorIndex !== 0,
|
||||
})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={() => {
|
||||
setEditor(editor)
|
||||
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
|
||||
metadata: { tier: pro ? 1 : 0, editor: editor.id },
|
||||
})
|
||||
}}
|
||||
onClick={() => {
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
|
||||
metadata: { tier: pro ? 1 : 0, editor: editor.id },
|
||||
})
|
||||
setEditor(editor)
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div>
|
||||
<img
|
||||
alt={editor.name}
|
||||
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
|
||||
width={34}
|
||||
className="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-muted mb-0 text-truncate" size="small">
|
||||
{editor.publisher}
|
||||
</Text>
|
||||
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
|
||||
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{group.length < 4
|
||||
? Array.from(new Array(4 - group.length).keys()).map(item => (
|
||||
<div key={item} className="flex-1 p-3" />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="d-flex justify-content-end align-items-center">
|
||||
<Text
|
||||
className="mb-0 text-muted cursor-pointer"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onCompleted()
|
||||
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'skip', {
|
||||
metadata: { tier: pro ? 1 : 0 },
|
||||
})
|
||||
}}
|
||||
>
|
||||
Skip for now
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
61
client/web/src/cody/onboarding/PurposeStep.tsx
Normal file
61
client/web/src/cody/onboarding/PurposeStep.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
|
||||
import { H2, Text } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { AuthenticatedUser } from '../../auth'
|
||||
import { HubSpotForm } from '../../marketing/components/HubSpotForm'
|
||||
|
||||
export function PurposeStep({
|
||||
onNext,
|
||||
pro,
|
||||
authenticatedUser,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onNext: () => void
|
||||
pro: boolean
|
||||
authenticatedUser: AuthenticatedUser
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
useEffect(() => {
|
||||
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
const primaryEmail = authenticatedUser.emails.find(email => email.isPrimary)?.email
|
||||
|
||||
const handleFormSubmit = (form: HTMLFormElement): void => {
|
||||
const choice = form[0].querySelector<HTMLInputElement>('input[name="cody_form_hand_raiser"]')
|
||||
|
||||
if (choice) {
|
||||
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'select', {
|
||||
metadata: { onboardingCall: choice.checked ? 1 : 0 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-bottom pb-3 mb-3">
|
||||
<H2 className="mb-1">Would you like to learn more about Cody for enterprise?</H2>
|
||||
<Text className="mb-0 text-muted" size="small">
|
||||
If you're not ready for a conversation, we'll stick to sharing Cody onboarding resources.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="d-flex align-items-center border-bottom mb-3 pb-3 justify-content-center">
|
||||
<HubSpotForm
|
||||
formId="19f34edd-1a98-4fc9-9b2b-c1edca727720"
|
||||
onFormSubmitted={() => {
|
||||
onNext()
|
||||
}}
|
||||
onFormLoadError={() => {
|
||||
onNext()
|
||||
}}
|
||||
userId={authenticatedUser.id}
|
||||
userEmail={primaryEmail}
|
||||
masterFormName="qualificationSurvey"
|
||||
onFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
75
client/web/src/cody/onboarding/WelcomeStep.tsx
Normal file
75
client/web/src/cody/onboarding/WelcomeStep.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { Text, Button } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { EventName } from '../../util/constants'
|
||||
|
||||
import styles from './CodyOnboarding.module.scss'
|
||||
|
||||
export function WelcomeStep({
|
||||
onNext,
|
||||
pro,
|
||||
telemetryRecorder,
|
||||
}: {
|
||||
onNext: () => void
|
||||
pro: boolean
|
||||
telemetryRecorder: TelemetryRecorder
|
||||
}): JSX.Element {
|
||||
const [show, setShow] = useState(false)
|
||||
const isLightTheme = useIsLightTheme()
|
||||
useEffect(() => {
|
||||
eventLogger.log(
|
||||
EventName.CODY_ONBOARDING_WELCOME_VIEWED,
|
||||
{ tier: pro ? 'pro' : 'free' },
|
||||
{ tier: pro ? 'pro' : 'free' }
|
||||
)
|
||||
telemetryRecorder.recordEvent('cody.onboarding.welcome', 'view', { metadata: { tier: pro ? 1 : 0 } })
|
||||
}, [pro, telemetryRecorder])
|
||||
|
||||
useEffect(() => {
|
||||
// theme is not ready on first render, it defaults to system theme.
|
||||
// so we need to wait a bit before showing the welcome video.
|
||||
setTimeout(() => {
|
||||
setShow(true)
|
||||
}, 500)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex flex-column align-items-center p-5')}>
|
||||
{show ? (
|
||||
<>
|
||||
<video width="180" className={classNames('mb-5', styles.welcomeVideo)} autoPlay={true} muted={true}>
|
||||
<source
|
||||
src={
|
||||
isLightTheme
|
||||
? 'https://storage.googleapis.com/sourcegraph-assets/hiCodyWhite.mp4'
|
||||
: 'https://storage.googleapis.com/sourcegraph-assets/hiCodyDark.mp4'
|
||||
}
|
||||
type="video/mp4"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<Text className={classNames('mb-4 pb-4', styles.fadeIn, styles.fadeSecond, styles.welcomeSubtitle)}>
|
||||
Ready to breeze through the basics and get comfortable with Cody
|
||||
{pro ? ' Pro' : ''}?
|
||||
</Text>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className={classNames(styles.fadeIn, styles.fadeThird)}
|
||||
>
|
||||
Sure, let's dive in!
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.blankPlaceholder} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user