Cody UI updates (#58828)

This commit is contained in:
Naman Kumar 2023-12-08 01:46:10 +05:30 committed by GitHub
parent 44224ffc93
commit 2dc7073cd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 218 additions and 75 deletions

View File

@ -28,3 +28,7 @@
.modal {
width: 50rem;
}
.release-stage {
color: var(--gray-07);
}

View File

@ -3,6 +3,7 @@ import type { ReactElement } from 'react'
import { mdiHelpCircleOutline, mdiTrendingUp, mdiDownload, mdiInformation } from '@mdi/js'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
import { useQuery, useMutation } from '@sourcegraph/http-client'
import {
@ -10,6 +11,7 @@ import {
PageHeader,
Link,
H4,
H5,
H2,
Text,
ButtonLink,
@ -18,6 +20,7 @@ import {
LoadingSpinner,
} from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { Page } from '../../components/Page'
import { PageTitle } from '../../components/PageTitle'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
@ -41,9 +44,13 @@ import styles from './CodyManagementPage.module.scss'
interface CodyManagementPageProps {
isSourcegraphDotCom: boolean
authenticatedUser: AuthenticatedUser | null
}
export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps> = ({ isSourcegraphDotCom }) => {
export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps> = ({
isSourcegraphDotCom,
authenticatedUser,
}) => {
const parameters = useSearchParameters()
const utm_source = parameters.get('utm_source')
@ -71,6 +78,14 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
}
}, [data?.currentUser, changeCodyPlan, enrollPro])
const navigate = useNavigate()
useEffect(() => {
if (!!data && !data?.currentUser) {
navigate('/sign-in?returnTo=/cody/manage')
}
}, [data, navigate])
if (!isCodyEnabled() || !isSourcegraphDotCom || !isEnabled || !data?.currentUser) {
return null
}
@ -147,7 +162,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
<LoadingSpinner />
)}
</div>
<H4 className="mb-0">Autocompletes</H4>
<H4 className="mb-0">Autocomplete suggestions</H4>
{!codyProEnabled && (
<Text className="text-muted mb-0" size="small">
this month
@ -178,7 +193,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
<LoadingSpinner />
)}
</div>
<H4 className="mb-0">Chat Messages</H4>
<H4 className="mb-0">Chat messages and commands</H4>
{!codyProEnabled && (
<Text className="text-muted mb-0" size="small">
this month
@ -194,7 +209,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
</Text>
</div>
<Text className="text-muted mb-0" size="small">
Until 14th of February 2023
Until 14th of February 2024
</Text>
</div>
)}
@ -208,10 +223,15 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
<Text className="text-muted mb-0">Cody integrates with your workflow.</Text>
</div>
<div>
<ButtonLink to="/cody/pricing" variant="secondary" outline={true} size="sm">
<Link
to="https://sourcegraph.com/community"
target="_blank"
rel="noreferrer"
className="text-muted text-sm"
>
<Icon svgPath={mdiHelpCircleOutline} className="mr-1" aria-hidden={true} />
Missing an editor?
</ButtonLink>
Have feedback? Join our community Discord to let us know!
</Link>
</div>
</div>
{editorGroups.map((group, index) => (
@ -228,7 +248,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
'border-left': index !== 0,
})}
>
<div className="d-flex mb-3">
<div className="d-flex mb-3 align-items-center">
<div>
<img
alt={editor.name}
@ -242,6 +262,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
{editor.publisher}
</Text>
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
</div>
</div>
<Link
@ -298,7 +319,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
))}
</div>
</Page>
<CodyOnboarding />
<CodyOnboarding authenticatedUser={authenticatedUser} />
</>
)
}

View File

@ -189,3 +189,8 @@
.fade-in-up-animation {
animation: 1.5s fadeInUp;
}
.blank-placeholder {
min-width: 11.25rem;
min-height: 17rem;
}

View File

@ -1,10 +1,13 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import classNames from 'classnames'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import { Modal, H5, H2, H3, Text, Button, useSearchParameters } from '@sourcegraph/wildcard'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { Modal, H5, H2, Text, Button, useSearchParameters } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { HubSpotForm } from '../../marketing/components/HubSpotForm'
import { eventLogger } from '../../tracking/eventLogger'
import { EventName } from '../../util/constants'
@ -37,20 +40,6 @@ export const editorGroups: IEditor[][] = [
releaseStage: 'Beta',
instructions: JetBrainsInstructions,
},
{
icon: 'NeoVim',
name: 'Neovim',
publisher: 'Neovim Team',
releaseStage: 'Experimental',
},
{
icon: 'AndroidStudio',
name: 'Android Studio',
publisher: 'Google',
releaseStage: 'Beta',
},
],
[
{
icon: 'PhpStorm',
name: 'PhpStorm ',
@ -63,6 +52,8 @@ export const editorGroups: IEditor[][] = [
publisher: 'Jetbrains',
releaseStage: 'Beta',
},
],
[
{
icon: 'WebStorm',
name: 'WebStorm',
@ -75,14 +66,26 @@ export const editorGroups: IEditor[][] = [
publisher: 'JetBrains',
releaseStage: 'Beta',
},
],
[
{
icon: 'GoLand',
name: 'GoLand',
publisher: 'JetBrains',
releaseStage: 'Beta',
},
{
icon: 'AndroidStudio',
name: 'Android Studio',
publisher: 'Google',
releaseStage: 'Beta',
},
],
[
{
icon: 'NeoVim',
name: 'Neovim',
publisher: 'Neovim Team',
releaseStage: 'Experimental',
},
{
icon: 'Emacs',
name: 'Emacs',
@ -92,7 +95,12 @@ export const editorGroups: IEditor[][] = [
],
]
export function CodyOnboarding(): JSX.Element | null {
export function CodyOnboarding({
authenticatedUser,
}: {
authenticatedUser: AuthenticatedUser | null
}): JSX.Element | null {
const [showEditorStep, setShowEditorStep] = useState(false)
const [completed = true, setOnboardingCompleted] = useTemporarySetting('cody.onboarding.completed', false)
// steps start from 0
const [step = -1, setOnboardingStep] = useTemporarySetting('cody.onboarding.step', 0)
@ -102,52 +110,134 @@ export function CodyOnboarding(): JSX.Element | null {
const parameters = useSearchParameters()
const enrollPro = parameters.get('pro') === 'true'
if (completed || step === -1 || step > 2) {
if (!showEditorStep && (completed || step === -1 || step > 1)) {
return null
}
if (!authenticatedUser) {
return null
}
return (
<Modal isOpen={true} aria-label="Cody Onboarding" className={styles.modal} position="center">
{step === 0 && <WelcomeStep onNext={onNext} pro={enrollPro} />}
{step === 1 && <PurposeStep onNext={onNext} pro={enrollPro} />}
{step === 2 && (
<EditorStep onNext={onNext} onCompleted={() => setOnboardingCompleted(true)} pro={enrollPro} />
{step === 1 && (
<PurposeStep
authenticatedUser={authenticatedUser}
onNext={() => {
onNext()
setOnboardingCompleted(true)
setShowEditorStep(true)
}}
pro={enrollPro}
/>
)}
{showEditorStep && (
<EditorStep
onCompleted={() => {
setShowEditorStep(false)
}}
pro={enrollPro}
/>
)}
</Modal>
)
}
function WelcomeStep({ onNext, pro }: { onNext: () => void; pro: boolean }): JSX.Element {
const [show, setShow] = useState(false)
const isLightTheme = useIsLightTheme()
useEffect(() => {
eventLogger.log(EventName.CODY_ONBOARDING_WELCOME_VIEWED, { tier: pro ? 'pro' : 'free' })
}, [pro])
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')}>
<video width="180" className={classNames('mb-5', styles.welcomeVideo)} autoPlay={true} muted={true}>
<source src="https://storage.googleapis.com/sourcegraph-assets/codyWelcomeAnim.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 ? ' to Cody Pro Trial' : ''}?
</Text>
<Button
onClick={onNext}
variant="primary"
size="lg"
className={classNames(styles.fadeIn, styles.fadeThird)}
>
Sure, let's dive in!
</Button>
{show ? (
<>
<video width="180" className={classNames('mb-5', styles.welcomeVideo)} autoPlay={true} muted={true}>
<source
src={
isLightTheme
? 'https://storage.googleapis.com/sourcegraph-assets/codyWelcomeAnim.mp4'
: 'https://storage.googleapis.com/sourcegraph-assets/codyWelcomeAnim_dark.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 ? ' to Cody Pro Trial' : ''}?
</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 }: { onNext: () => void; pro: boolean }): JSX.Element {
function PurposeStep({
onNext,
pro,
authenticatedUser,
}: {
onNext: () => void
pro: boolean
authenticatedUser: AuthenticatedUser
}): JSX.Element {
const [useCase, setUseCase] = useState<'work' | 'personal' | null>(null)
useEffect(() => {
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_VIEWED, { tier: pro ? 'pro' : 'free' })
}, [pro])
const primaryEmail = authenticatedUser.emails.find(email => email.isPrimary)?.email
const handleFormReady = useCallback((form: HTMLFormElement) => {
const workInput = form.querySelector('input[name="using_cody_for_work"]')
const personalInput = form.querySelector('input[name="using_cody_for_personal"]')
const handleChange = (e: Event): void => {
const target = e.target as HTMLInputElement
const isChecked = target.checked
const name = target.name
if (name === 'using_cody_for_work' && isChecked) {
setUseCase('work')
} else if (name === 'using_cody_for_personal' && isChecked) {
setUseCase('personal')
} else {
setUseCase(null)
}
}
workInput?.addEventListener('change', handleChange)
personalInput?.addEventListener('change', handleChange)
return () => {
workInput?.removeEventListener('change', handleChange)
personalInput?.removeEventListener('change', handleChange)
}
}, [])
return (
<>
<div className="border-bottom pb-3 mb-3">
@ -156,16 +246,32 @@ function PurposeStep({ onNext, pro }: { onNext: () => void; pro: boolean }): JSX
This will allow us to understand our audience better and guide your journey
</Text>
</div>
<div className="d-flex align-items-center border-bottom mb-3 pb-3">
<div className="d-flex align-items-center border-bottom mb-3 pb-3 justify-content-center">
<HubSpotForm
formId="85548efc-a879-4553-9ef0-a8da8fdcf541"
onFormSubmitted={() => {
if (useCase) {
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_SELECTED, { useCase })
}
onNext()
}}
userId={authenticatedUser.id}
userEmail={primaryEmail}
masterFormName="qualificationSurvey"
onFormReady={handleFormReady}
/>
{/* TODO(naman): remove after PR feedback
<div
role="button"
tabIndex={0}
onKeyDown={() => {
onKeyDown={event => {
event.preventDefault()
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_SELECTED, { useCase: 'work' })
onNext()
}}
className="border-right flex-1 d-flex flex-column justify-content-center cursor-pointer align-items-center py-3 px-2"
onClick={() => {
onClick={event => {
event.preventDefault()
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_SELECTED, { useCase: 'work' })
onNext()
}}
@ -177,11 +283,13 @@ function PurposeStep({ onNext, pro }: { onNext: () => void; pro: boolean }): JSX
role="button"
tabIndex={0}
className="flex-1 d-flex flex-column justify-content-center cursor-pointer align-items-center py-3 px-2"
onKeyDown={() => {
onKeyDown={event => {
event.preventDefault()
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_SELECTED, { useCase: 'personal' })
onNext()
}}
onClick={() => {
onClick={event => {
event.preventDefault()
eventLogger.log(EventName.CODY_ONBOARDING_PURPOSE_SELECTED, { useCase: 'personal' })
onNext()
}}
@ -189,6 +297,7 @@ function PurposeStep({ onNext, pro }: { onNext: () => void; pro: boolean }): JSX
<PersonalIcon />
<H3 className="mb-0 mt-2">Personal projects</H3>
</div>
*/}
</div>
<Text size="small" className="text-muted text-center mb-0">
Pick one to move forward
@ -197,15 +306,7 @@ function PurposeStep({ onNext, pro }: { onNext: () => void; pro: boolean }): JSX
)
}
function EditorStep({
onNext,
onCompleted,
pro,
}: {
onNext: () => void
onCompleted: () => void
pro: boolean
}): JSX.Element {
function EditorStep({ onCompleted, pro }: { onCompleted: () => void; pro: boolean }): JSX.Element {
useEffect(() => {
eventLogger.log(EventName.CODY_ONBOARDING_CHOOSE_EDITOR_VIEWED, { tier: pro ? 'pro' : 'free' })
}, [pro])
@ -287,7 +388,7 @@ function EditorStep({
</div>
))}
</div>
<div className="d-flex justify-content-between align-items-center">
<div className="d-flex justify-content-end align-items-center">
<Text
className="mb-0 text-muted cursor-pointer"
size="small"
@ -296,16 +397,15 @@ function EditorStep({
eventLogger.log(EventName.CODY_ONBOARDING_CHOOSE_EDITOR_SKIPPED, { tier: pro ? 'pro' : 'free' })
}}
>
Skip
</Text>
<Text className="mb-0 text-muted" size="small">
Pick one to move forward
Skip for now
</Text>
</div>
</>
)
}
/* TODO(naman): remove after PR feedback
const WorkIcon = (): JSX.Element => (
<svg width="60" height="60" viewBox="0 0 75 75" fill="none">
<path
@ -363,3 +463,4 @@ const PersonalIcon = (): JSX.Element => (
</defs>
</svg>
)
*/

View File

@ -47,6 +47,7 @@ export function JetBrainsInstructions({
<ButtonLink
variant="primary"
to="https://marketplace.visualstudio.com/items?itemName=sourcegraph.cody-ai"
target="_blank"
>
Open Marketplace
</ButtonLink>

View File

@ -3,6 +3,7 @@ import type { ReactElement } from 'react'
import { mdiTrendingUp } from '@mdi/js'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@sourcegraph/http-client'
import { Icon, PageHeader, Button, H1, H2, H3, Text, ButtonLink, useSearchParameters, H4 } from '@sourcegraph/wildcard'
@ -46,6 +47,14 @@ export const CodySubscriptionPage: React.FunctionComponent<CodySubscriptionPageP
const [showUpgradeToPro, setShowUpgradeToPro] = useState<boolean>(false)
const [showCancelPro, setShowCancelPro] = useState<boolean>(false)
const navigate = useNavigate()
useEffect(() => {
if (!!data && !data?.currentUser) {
navigate('/sign-in?returnTo=/cody/subscription')
}
}, [data, navigate])
if (!isCodyEnabled() || !isSourcegraphDotCom || !isEnabled || !data?.currentUser || !authenticatedUser) {
return null
}
@ -225,6 +234,8 @@ export const CodySubscriptionPage: React.FunctionComponent<CodySubscriptionPageP
className="flex-1 mt-3"
variant="secondary"
outline={true}
to="https://sourcegraph.com/contact/request-info?utm_source=cody_subscription_page"
target="_blank"
onClick={() => {
eventLogger.log(EventName.CODY_SUBSCRIPTION_PLAN_CLICKED, { tier: 'enterprise' })
}}

View File

@ -8,8 +8,8 @@
position: relative;
text-align: center;
transition: all 0.15s linear;
background-color: var(--violet-04) !important;
border-color: var(--violet-04) !important;
background-color: #0b70db !important;
border-color: #0b70db !important;
color: var(--white);
border-radius: 0.3125rem;
border-style: solid;
@ -19,13 +19,13 @@
&:hover,
&:focus {
background-color: var(--violet-05) !important;
border-color: var(--violet-05) !important;
background-color: #0864c6 !important;
border-color: #0864c6 !important;
}
&:active {
background-color: var(--violet-05) !important;
border-color: var(--violet-05) !important;
background-color: #0864c6 !important;
border-color: #0864c6 !important;
}
}
@ -98,7 +98,7 @@
&:focus {
outline: none;
border-color: var(--violet-04);
border-color: #0b70db;
}
}
@ -115,7 +115,7 @@
:global(.hs-input)[type='checkbox']:checked,
:global(.hs-input)[type='radio']:checked {
accent-color: var(--violet-04);
accent-color: #0b70db;
}
:global(.actions) {