Add enterprise UIs for cody enablement (#53360)

This PR implements
https://www.figma.com/file/ZpFPw0zZhVkCjrp68hPmA6/Enterprise-Cody-web-onboarding?type=design&node-id=0-1&t=sHIWi49gJ0ionqkp-0
for Cody enablement and simplifies the feature flags used to determine
when which of these should show. We now simply look at if cody is
enabled instead of multiple feature flags.

Closes https://github.com/sourcegraph/sourcegraph/issues/53023

Closes https://github.com/sourcegraph/sourcegraph/issues/52546

## Test plan

Manually verified.

---------

Co-authored-by: Rob Rhyne <rob@sourcegraph.com>
This commit is contained in:
Erik Seliger 2023-06-16 03:35:58 +02:00 committed by GitHub
parent e35a52d8d7
commit da262c68b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 651 additions and 357 deletions

View File

@ -41,8 +41,10 @@ interface ChatProps extends ChatClassNames {
setSuggestions?: (suggestions: undefined | []) => void
needsEmailVerification?: boolean
needsEmailVerificationNotice?: React.FunctionComponent
codyNotEnabledNotice?: React.FunctionComponent
abortMessageInProgressComponent?: React.FunctionComponent<{ onAbortMessageInProgress: () => void }>
onAbortMessageInProgress?: () => void
isCodyEnabled: boolean
}
interface ChatClassNames extends TranscriptItemClassNames {
@ -127,11 +129,13 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
suggestions,
setSuggestions,
needsEmailVerification = false,
codyNotEnabledNotice: CodyNotEnabledNotice,
needsEmailVerificationNotice: NeedsEmailVerificationNotice,
contextStatusComponent: ContextStatusComponent,
contextStatusComponentProps = {},
abortMessageInProgressComponent,
onAbortMessageInProgress,
isCodyEnabled,
}) => {
const [inputRows, setInputRows] = useState(5)
const [historyIndex, setHistoryIndex] = useState(inputHistory.length)
@ -232,7 +236,11 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
return (
<div className={classNames(className, styles.innerContainer)}>
{needsEmailVerification && NeedsEmailVerificationNotice ? (
{!isCodyEnabled && CodyNotEnabledNotice ? (
<div className="flex-1">
<CodyNotEnabledNotice />
</div>
) : needsEmailVerification && NeedsEmailVerificationNotice ? (
<div className="flex-1">
<NeedsEmailVerificationNotice />
</div>
@ -281,17 +289,17 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
<TextArea
className={classNames(styles.chatInput, chatInputClassName)}
rows={inputRows}
value={formInput}
value={isCodyEnabled ? formInput : 'Cody is disabled on this instance'}
autoFocus={true}
required={true}
disabled={needsEmailVerification}
disabled={needsEmailVerification || !isCodyEnabled}
onInput={onChatInput}
onKeyDown={onChatKeyDown}
/>
<SubmitButton
className={styles.submitButton}
onClick={onChatSubmit}
disabled={!!messageInProgress || needsEmailVerification}
disabled={!!messageInProgress || needsEmailVerification || !isCodyEnabled}
/>
</div>
{ContextStatusComponent ? (

View File

@ -106,6 +106,7 @@ export const App: React.FunctionComponent = () => {
setFormInput={setFormInput}
inputHistory={inputHistory}
setInputHistory={setInputHistory}
isCodyEnabled={true}
onSubmit={onSubmit}
/>
</>

View File

@ -49,6 +49,7 @@ export const Chat: React.FunctionComponent<
transcriptActionClassName={styles.transcriptAction}
inputRowClassName={styles.inputRow}
chatInputClassName={styles.chatInput}
isCodyEnabled={true}
/>
)

View File

@ -114,6 +114,10 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
copyButtonOnSubmit={onCopyBtnClick}
suggestions={suggestions}
setSuggestions={setSuggestions}
// TODO: We should fetch this from the server and pass a pretty component
// down here to render cody is disabled on the instance nicely.
isCodyEnabled={true}
codyNotEnabledNotice={undefined}
/>
)
}

View File

@ -228,6 +228,7 @@ ts_project(
"src/cody/components/ScopeSelector/ScopeSelector.tsx",
"src/cody/components/ScopeSelector/backend.ts",
"src/cody/components/ScopeSelector/index.tsx",
"src/cody/isCodyEnabled.tsx",
"src/cody/search/CodySearchPage.tsx",
"src/cody/search/api.ts",
"src/cody/search/translateToQuery.ts",
@ -236,7 +237,6 @@ ts_project(
"src/cody/sidebar/index.tsx",
"src/cody/sidebar/useSidebarSize.tsx",
"src/cody/useCodyChat.tsx",
"src/cody/useIsCodyEnabled.tsx",
"src/cody/useSizebarSize.tsx",
"src/cody/widgets/CodyRecipesWidget.tsx",
"src/cody/widgets/components/Recipe.tsx",

View File

@ -147,3 +147,8 @@
.mobile-history {
overflow-y: auto;
}
.cta-content {
background-color: var(--color-bg-1);
padding: 1rem;
}

View File

@ -18,7 +18,6 @@ import { CodyLogo } from '@sourcegraph/cody-ui/src/icons/CodyLogo'
import { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import {
Alert,
Badge,
Button,
Icon,
@ -33,6 +32,7 @@ import {
H4,
H3,
Text,
ButtonLink,
Tooltip,
} from '@sourcegraph/wildcard'
@ -45,6 +45,7 @@ import { EventName } from '../../util/constants'
import { ChatUI } from '../components/ChatUI'
import { CodyMarketingPage } from '../components/CodyMarketingPage'
import { HistoryList } from '../components/HistoryList'
import { isCodyEnabled } from '../isCodyEnabled'
import { CodyChatStore, useCodyChat } from '../useCodyChat'
import { CodyColorIcon } from './CodyPageIcon'
@ -52,6 +53,7 @@ import { CodyColorIcon } from './CodyPageIcon'
import styles from './CodyChatPage.module.scss'
interface CodyChatPageProps {
isSourcegraphDotCom: boolean
authenticatedUser: AuthenticatedUser | null
isSourcegraphApp: boolean
context: Pick<SourcegraphContext, 'authProviders'>
@ -94,6 +96,7 @@ const onTranscriptHistoryLoad = (
export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
authenticatedUser,
context,
isSourcegraphDotCom,
isSourcegraphApp,
}) => {
const { pathname } = useLocation()
@ -106,7 +109,6 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
const {
initializeNewChat,
clearHistory,
isCodyEnabled,
loaded,
transcript,
transcriptHistory,
@ -146,22 +148,40 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
return null
}
if (!authenticatedUser) {
return <CodyMarketingPage context={context} />
}
if (!isCodyEnabled.chat) {
return (
<Page className="overflow-hidden">
<PageTitle title="Cody AI Chat" />
<Alert variant="info">Cody is not enabled. Please contact your site admin to enable Cody.</Alert>
</Page>
)
if (!authenticatedUser || !isCodyEnabled()) {
return <CodyMarketingPage isSourcegraphDotCom={isSourcegraphDotCom} context={context} />
}
return (
<Page className={classNames('d-flex flex-column', styles.page)}>
<PageTitle title="Cody AI Chat" />
{!isSourcegraphDotCom && !isCTADismissed && (
<MarketingBlock
wrapperClassName="mb-5"
contentClassName={classNames(styles.ctaWrapper, styles.ctaContent)}
>
<div className="d-flex">
<CodyCTAIcon className="flex-shrink-0" />
<div className="ml-3">
<H3>Cody is more powerful in your IDE</H3>
<Text>
Cody adds powerful AI assistant functionality like inline completions and assist, and
powerful recipes to help you understand codebases and generate and fix code more
accurately.
</Text>
<ButtonLink variant="primary" to="/help/cody#get-cody">
View editor extensions &rarr;
</ButtonLink>
</div>
</div>
<Icon
svgPath={mdiClose}
aria-label="Close Cody editor extensions CTA"
className={classNames(styles.closeButton, 'position-absolute m-0')}
onClick={onCTADismiss}
/>
</MarketingBlock>
)}
<PageHeader
actions={
<div className="d-flex">
@ -173,8 +193,14 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
}
description={
<>
Cody answers code questions and writes code for you by reading your entire codebase and the code
graph.
Cody answers code questions and writes code for you by leveraging your entire codebase and the
code graph.
{!isSourcegraphDotCom && isCTADismissed && (
<>
{' '}
<Link to="/help/cody#get-cody">Cody is more powerful in the IDE</Link>.
</>
)}
</>
}
className={styles.pageHeader}
@ -192,7 +218,6 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
</PageHeader.Breadcrumb>
</PageHeader.Heading>
</PageHeader>
{/* Page content */}
<div className={classNames('row flex-1 overflow-hidden', styles.pageWrapper)}>
<div className={classNames('col-md-3', styles.sidebarWrapper)}>
@ -230,7 +255,8 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
deleteHistoryItem={deleteHistoryItem}
/>
</div>
{!isCTADismissed &&
{isSourcegraphDotCom &&
!isCTADismissed &&
(showVSCodeCTA ? (
<MarketingBlock
wrapperClassName="d-flex"
@ -426,3 +452,38 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
</Page>
)
}
const CodyCTAIcon: React.FunctionComponent<{ className?: string }> = ({ className }) => (
<svg
width="146"
height="112"
viewBox="0 0 146 112"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect x="24" y="24" width="98" height="64" rx="6" fill="#E8D1FF" />
<path
d="M56.25 65.3333C56.25 65.687 56.3817 66.0261 56.6161 66.2761C56.8505 66.5262 57.1685 66.6667 57.5 66.6667H60V69.3333H56.875C56.1875 69.3333 55 68.7333 55 68C55 68.7333 53.8125 69.3333 53.125 69.3333H50V66.6667H52.5C52.8315 66.6667 53.1495 66.5262 53.3839 66.2761C53.6183 66.0261 53.75 65.687 53.75 65.3333V46.6667C53.75 46.313 53.6183 45.9739 53.3839 45.7239C53.1495 45.4738 52.8315 45.3333 52.5 45.3333H50V42.6667H53.125C53.8125 42.6667 55 43.2667 55 44C55 43.2667 56.1875 42.6667 56.875 42.6667H60V45.3333H57.5C57.1685 45.3333 56.8505 45.4738 56.6161 45.7239C56.3817 45.9739 56.25 46.313 56.25 46.6667V65.3333Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M88.9095 45C90.3781 45 91.5686 46.1789 91.5686 47.6331V52.314C91.5686 53.7682 90.3781 54.9471 88.9095 54.9471C87.4409 54.9471 86.2504 53.7682 86.2504 52.314V47.6331C86.2504 46.1789 87.4409 45 88.9095 45Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M72.068 51.1437C72.068 49.6895 73.2585 48.5106 74.7271 48.5106H79.4544C80.923 48.5106 82.1135 49.6895 82.1135 51.1437C82.1135 52.5978 80.923 53.7767 79.4544 53.7767H74.7271C73.2585 53.7767 72.068 52.5978 72.068 51.1437Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M95.2643 58.8091C96.2107 59.6994 96.2491 61.1808 95.35 62.1179L94.5134 62.99C87.9666 69.8138 76.9295 69.6438 70.6002 62.6216C69.731 61.6572 69.8159 60.1777 70.7898 59.317C71.7637 58.4563 73.2579 58.5403 74.1271 59.5047C78.6157 64.4848 86.4432 64.6053 91.0861 59.7659L91.9227 58.8939C92.8218 57.9568 94.3179 57.9188 95.2643 58.8091Z"
fill="#A305E1"
/>
</svg>
)

View File

@ -152,3 +152,7 @@
width: 1.5rem;
}
}
.not-enabled-block {
background-color: var(--body-bg);
}

View File

@ -17,6 +17,7 @@ import { Button, Icon, TextArea, Link, Tooltip, Alert, Text, H2 } from '@sourceg
import { eventLogger } from '../../../tracking/eventLogger'
import { CodyPageIcon } from '../../chat/CodyPageIcon'
import { isCodyEnabled, isEmailVerificationNeededForCody } from '../../isCodyEnabled'
import { useCodySidebar } from '../../sidebar/Provider'
import { CodyChatStore } from '../../useCodyChat'
import { ScopeSelector } from '../ScopeSelector'
@ -40,7 +41,6 @@ export const ChatUI: React.FC<IChatUIProps> = ({ codyChatStore }): JSX.Element =
transcript,
transcriptHistory,
loaded,
isCodyEnabled,
scope,
setScope,
toggleIncludeInferredRepository,
@ -99,12 +99,14 @@ export const ChatUI: React.FC<IChatUIProps> = ({ codyChatStore }): JSX.Element =
transcriptActionClassName={styles.transcriptAction}
FeedbackButtonsContainer={FeedbackButtons}
feedbackButtonsOnSubmit={onFeedbackSubmit}
needsEmailVerification={isCodyEnabled.needsEmailVerification}
needsEmailVerification={isEmailVerificationNeededForCody()}
needsEmailVerificationNotice={NeedsEmailVerificationNotice}
codyNotEnabledNotice={CodyNotEnabledNotice}
contextStatusComponent={ScopeSelector}
contextStatusComponentProps={scopeSelectorProps}
abortMessageInProgressComponent={AbortMessageInProgress}
onAbortMessageInProgress={abortMessageInProgress}
isCodyEnabled={isCodyEnabled()}
/>
</>
)
@ -240,7 +242,7 @@ interface AutoResizableTextAreaProps extends ChatUITextAreaProps {}
export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = React.memo(
function AutoResizableTextAreaContent({ value, onInput, onKeyDown, className, disabled = false }) {
const { inputNeedsFocus, setFocusProvided, isCodyEnabled } = useCodySidebar() || {
const { inputNeedsFocus, setFocusProvided } = useCodySidebar() || {
inputNeedsFocus: false,
setFocusProvided: () => null,
}
@ -280,7 +282,7 @@ export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = React
}
return (
<Tooltip content={isCodyEnabled.needsEmailVerification ? 'Verify your email to use Cody.' : ''}>
<Tooltip content={isEmailVerificationNeededForCody() ? 'Verify your email to use Cody.' : ''}>
<TextArea
ref={textAreaRef}
className={className}
@ -319,3 +321,51 @@ const NeedsEmailVerificationNotice: React.FunctionComponent = React.memo(
)
}
)
const CodyNotEnabledNotice: React.FunctionComponent = React.memo(function CodyNotEnabledNoticeContent() {
return (
<div className={classNames('p-3', styles.notEnabledBlock)}>
<H2 className={classNames('d-flex gap-1 align-items-center mb-3', styles.codyMessageHeader)}>
<CodyPageIcon /> Cody
</H2>
<div className="d-flex align-items-start">
<CodyNotEnabledIcon className="flex-shrink-0" />
<Text className="ml-2">
Cody isn't available on this instance, but you can learn more about Cody{' '}
<Link to="https://about.sourcegraph.com/cody?utm_source=server">here</Link>.
</Text>
</div>
</div>
)
})
const CodyNotEnabledIcon: React.FunctionComponent<{ className?: string }> = ({ className }) => (
<svg
width="36"
height="43"
viewBox="0 0 36 43"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect y="4" width="36" height="35" rx="4.125" fill="#E8D1FF" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.368 15.2742C22.1732 15.2742 22.826 15.9206 22.826 16.7179V19.2844C22.826 20.0818 22.1732 20.7281 21.368 20.7281C20.5628 20.7281 19.91 20.0818 19.91 19.2844V16.7179C19.91 15.9206 20.5628 15.2742 21.368 15.2742Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.1339 18.6427C12.1339 17.8454 12.7866 17.199 13.5919 17.199H16.1838C16.989 17.199 17.6418 17.8454 17.6418 18.6427C17.6418 19.4401 16.989 20.0864 16.1838 20.0864H13.5919C12.7866 20.0864 12.1339 19.4401 12.1339 18.6427Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24.8523 22.8456C25.3712 23.3338 25.3923 24.146 24.8993 24.6599L24.4406 25.138C20.851 28.8795 14.7994 28.7863 11.3291 24.9361C10.8525 24.4073 10.899 23.5961 11.433 23.1241C11.967 22.6522 12.7863 22.6983 13.2629 23.2271C15.724 25.9576 20.0157 26.0237 22.5614 23.3703L23.0201 22.8922C23.5131 22.3783 24.3334 22.3575 24.8523 22.8456Z"
fill="#A305E1"
/>
</svg>
)

View File

@ -55,6 +55,7 @@
}
.cody-sign-up-panel {
background-color: var(--color-bg-1);
padding: 1.5rem;
box-shadow: 0 0.5rem 1rem -6px rgba(46, 34, 119, 0.1);
border-radius: 3px;
@ -116,6 +117,7 @@
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
padding-bottom: 4rem;
.learn-more-items {
padding: 1rem;
@ -169,6 +171,7 @@
.cody-platform-card-wrapper {
padding: 1rem;
border-radius: 0.25rem;
background-color: var(--color-bg-1);
:global(.theme-dark) & {
border: 1px solid var(--gray-09);

View File

@ -30,4 +30,9 @@ const context: Pick<SourcegraphContext, 'authProviders'> = {
],
}
export const Default: Story = () => <WebStory>{() => <CodyMarketingPage context={context} />}</WebStory>
export const SourcegraphDotCom: Story = () => (
<WebStory>{() => <CodyMarketingPage context={context} isSourcegraphDotCom={true} />}</WebStory>
)
export const Enterprise: Story = () => (
<WebStory>{() => <CodyMarketingPage context={context} isSourcegraphDotCom={false} />}</WebStory>
)

View File

@ -1,4 +1,4 @@
import { mdiChevronRight, mdiCodeBracesBox, mdiGit, mdiMicrosoftVisualStudioCode } from '@mdi/js'
import { mdiChevronRight, mdiCodeBracesBox, mdiGit } from '@mdi/js'
import classNames from 'classnames'
import { Theme, useTheme } from '@sourcegraph/shared/src/theme'
@ -9,6 +9,7 @@ import { MarketingBlock } from '../../../components/MarketingBlock'
import { Page } from '../../../components/Page'
import { PageTitle } from '../../../components/PageTitle'
import { SourcegraphContext } from '../../../jscontext'
import { MeetCodySVG } from '../../../repo/components/TryCodyWidget/WidgetIcons'
import { eventLogger } from '../../../tracking/eventLogger'
import { EventName } from '../../../util/constants'
import { CodyColorIcon, CodyHelpIcon, CodyWorkIcon } from '../../chat/CodyPageIcon'
@ -16,9 +17,9 @@ import { CodyColorIcon, CodyHelpIcon, CodyWorkIcon } from '../../chat/CodyPageIc
import styles from './CodyMarketingPage.module.scss'
interface CodyPlatformCardProps {
icon: string
icon: string | JSX.Element
title: string
description: string
description: string | JSX.Element
illustration: string
}
@ -26,7 +27,24 @@ const onSpeakToAnEngineer = (): void => eventLogger.log(EventName.SPEAK_TO_AN_EN
const onClickCTAButton = (type: string): void =>
eventLogger.log('SignupInitiated', { type, source: 'cody-signed-out' }, { type, page: 'cody-signed-out' })
const codyPlatformCardItems = [
const IDEIcon: React.FunctionComponent<{}> = () => (
<svg viewBox="-4 -4 31 31" fill="none" xmlns="http://www.w3.org/2000/svg" className={styles.codyPlatformCardIcon}>
<rect x="0.811523" y="0.366669" width="25" height="25" rx="3" fill="#4D52F4" />
<path
d="M13.8115 20.1583C13.8115 20.4346 13.9169 20.6996 14.1044 20.8949C14.292 21.0903 14.5463 21.2 14.8115 21.2H16.8115V23.2833H14.3115C13.7615 23.2833 12.8115 22.8146 12.8115 22.2417C12.8115 22.8146 11.8615 23.2833 11.3115 23.2833H8.81152V21.2H10.8115C11.0767 21.2 11.3311 21.0903 11.5186 20.8949C11.7062 20.6996 11.8115 20.4346 11.8115 20.1583V5.57501C11.8115 5.29874 11.7062 5.03379 11.5186 4.83844C11.3311 4.64309 11.0767 4.53335 10.8115 4.53335H8.81152V2.45001H11.3115C11.8615 2.45001 12.8115 2.91876 12.8115 3.49168C12.8115 2.91876 13.7615 2.45001 14.3115 2.45001H16.8115V4.53335H14.8115C14.5463 4.53335 14.292 4.64309 14.1044 4.83844C13.9169 5.03379 13.8115 5.29874 13.8115 5.57501V20.1583Z"
fill="white"
/>
</svg>
)
const codyPlatformCardItems = (
isSourcegraphDotCom: boolean
): {
title: string
description: string | JSX.Element
icon: string | JSX.Element
illustration: { dark: string; light: string }
}[] => [
{
title: 'Knows your code',
description:
@ -38,31 +56,55 @@ const codyPlatformCardItems = [
},
},
{
title: 'Works with VS Code',
description:
'This extension combines an LLM with the context of your code to help you generate and fix code more accurately.',
icon: mdiMicrosoftVisualStudioCode,
title: 'More powerful in your IDE',
description: (
<>
The extensions combine an LLM with the context of your code to help you generate and fix code more
accurately. <Link to="/help/cody#get-cody">View supported IDEs</Link>.
</>
),
icon: <IDEIcon />,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-vs-code-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-vs-code-illustration-light.png',
},
},
{
title: 'Try it on sourcegraph.com',
description: 'Cody explains, generates, convert code, and more within the context of public repositories.',
icon: mdiGit,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-light.png',
},
},
...(isSourcegraphDotCom
? [
{
title: 'Try it on sourcegraph.com',
description:
'Cody explains, generates, convert code, and more within the context of public repositories.',
icon: mdiGit,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-light.png',
},
},
]
: [
{
title: 'Recipes accelerate your flow',
description:
'Cody explains, generates, convert code, and more within the context of your repositories.',
icon: mdiGit,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-light.png',
},
},
]),
]
export interface CodyMarketingPageProps {
isSourcegraphDotCom: boolean
context: Pick<SourcegraphContext, 'authProviders'>
}
export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps> = ({ context }) => {
export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps> = ({
context,
isSourcegraphDotCom,
}) => {
const { theme } = useTheme()
const isDarkTheme = theme === Theme.Dark
@ -90,7 +132,7 @@ export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps>
{/* Page content */}
<div className={styles.headerSection}>
<div>
<H1>Meet Cody, your AI assistant</H1>
{isSourcegraphDotCom && <H1>Meet Cody, your AI assistant</H1>}
<div className="ml-3">
<Text className={styles.codyConversation}>Auto-generate unit tests</Text>
<Text className={styles.codyConversation}>Explain code</Text>
@ -100,49 +142,70 @@ export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps>
<CodyHelpIcon className={styles.codyHelpIcon} />
</div>
<MarketingBlock
contentClassName={styles.codySignUpPanel}
wrapperClassName={styles.codySignUpPanelWrapper}
>
<H2>Sign up to get free access</H2>
<Text className="mt-3">
Cody combines an LLM with the context of Sourcegraph's code graph on public code or your code at
work. Sign up with:
</Text>
<div className={styles.buttonWrapper}>
<ExternalsAuth
context={context}
githubLabel="GitHub"
gitlabLabel="GitLab"
withCenteredText={true}
onClick={onClickCTAButton}
ctaClassName={styles.authButton}
iconClassName={styles.buttonIcon}
/>
</div>
<Link to="https://sourcegraph.com/sign-up?showEmail=true">Or, continue with email</Link>
<Text className="mt-3 mb-0">
By registering, you agree to our{' '}
<Link
className={styles.termsPrivacyLink}
to="https://about.sourcegraph.com/terms"
target="_blank"
rel="noopener"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
className={styles.termsPrivacyLink}
to="https://about.sourcegraph.com/privacy"
target="_blank"
rel="noopener"
>
Privacy Policy
</Link>
.
</Text>
</MarketingBlock>
{isSourcegraphDotCom ? (
<MarketingBlock
contentClassName={styles.codySignUpPanel}
wrapperClassName={styles.codySignUpPanelWrapper}
>
<H2>Sign up to get free access</H2>
<Text className="mt-3">
Cody combines an LLM with the context of Sourcegraph's code graph on public code or your
code at work. Sign up with:
</Text>
<div className={styles.buttonWrapper}>
<ExternalsAuth
context={context}
githubLabel="GitHub"
gitlabLabel="GitLab"
withCenteredText={true}
onClick={onClickCTAButton}
ctaClassName={styles.authButton}
iconClassName={styles.buttonIcon}
/>
</div>
<Link to="https://sourcegraph.com/sign-up?showEmail=true">Or, continue with email</Link>
<Text className="mt-3 mb-0">
By registering, you agree to our{' '}
<Link
className={styles.termsPrivacyLink}
to="https://about.sourcegraph.com/terms"
target="_blank"
rel="noopener"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
className={styles.termsPrivacyLink}
to="https://about.sourcegraph.com/privacy"
target="_blank"
rel="noopener"
>
Privacy Policy
</Link>
.
</Text>
</MarketingBlock>
) : (
<MarketingBlock
contentClassName={styles.codySignUpPanel}
wrapperClassName={styles.codySignUpPanelWrapper}
>
<H2>Meet Cody, your AI assistant</H2>
<Text className="mt-3">
Cody is an AI assistant that leverages the code graph to know more about your code. Use it
to:
</Text>
<ul>
<li>Onboard to new codebases</li>
<li>Evaluate and fix code</li>
<li>Write code faster</li>
</ul>
<Text className="mb-0">
<Link to="https://about.sourcegraph.com/cody">Learn more about Cody &rarr;</Link>
</Text>
</MarketingBlock>
)}
</div>
<div className={styles.learnMoreSection}>
@ -154,7 +217,7 @@ export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps>
styles.startingPointWrapper
)}
>
{codyPlatformCardItems.map(item => (
{codyPlatformCardItems(isSourcegraphDotCom).map(item => (
<CodyPlatformCard
key={item.title}
title={item.title}
@ -166,16 +229,28 @@ export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps>
</div>
<div className={styles.learnMoreItemsWrapper}>
<div className={styles.learnMoreItems}>
<H4 className={styles.learnMoreItemsTitle}>Overview</H4>
<Text className="mb-0">
Visit the{' '}
<Link to="https://about.sourcegraph.com/cody" target="_blank" rel="noopener">
product page
</Link>{' '}
and see what devs are building with Cody.
</Text>
</div>
{isSourcegraphDotCom ? (
<div className={styles.learnMoreItems}>
<H4 className={styles.learnMoreItemsTitle}>Overview</H4>
<Text className="mb-0">
Visit the{' '}
<Link to="https://about.sourcegraph.com/cody" target="_blank" rel="noopener">
product page
</Link>{' '}
and see what devs are building with Cody.
</Text>
</div>
) : (
<div className="d-flex align-items-center">
<div>
<MeetCodySVG />
</div>
<Text className="ml-3">
<Link to="https://about.sourcegraph.com/cody">Learn about Cody</Link>, Sourcegraph's AI
coding assistant.
</Text>
</div>
)}
<div className={styles.learnMoreItems}>
<H4 className={styles.learnMoreItemsTitle}>Documentation</H4>
@ -190,26 +265,29 @@ export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps>
</div>
</div>
<div className={styles.footer}>
<CodyWorkIcon />
<div>
<H1 className="mb-2">Get Cody for work</H1>
<Text className={styles.footerDescription}>
Cody for Sourcegraph Enterprise utilizes Sourcegraph's code graph to deliver context-aware
answers based on your private codebase, enabling enhanced code comprehension and productivity.
</Text>
<div className="mb-2">
<Link
to="https://about.sourcegraph.com/demo"
className={classNames('d-inline-flex align-items-center', styles.footerCtaLink)}
onClick={onSpeakToAnEngineer}
>
Speak to an engineer
<Icon svgPath={mdiChevronRight} aria-hidden={true} />
</Link>
{isSourcegraphDotCom && (
<div className={styles.footer}>
<CodyWorkIcon />
<div>
<H1 className="mb-2">Get Cody for work</H1>
<Text className={styles.footerDescription}>
Cody for Sourcegraph Enterprise utilizes Sourcegraph's code graph to deliver context-aware
answers based on your private codebase, enabling enhanced code comprehension and
productivity.
</Text>
<div className="mb-2">
<Link
to="https://about.sourcegraph.com/demo"
className={classNames('d-inline-flex align-items-center', styles.footerCtaLink)}
onClick={onSpeakToAnEngineer}
>
Speak to an engineer
<Icon svgPath={mdiChevronRight} aria-hidden={true} />
</Link>
</div>
</div>
</div>
</div>
)}
</Page>
)
}
@ -222,7 +300,11 @@ const CodyPlatformCard: React.FunctionComponent<CodyPlatformCardProps> = ({
}) => (
<div className={styles.codyPlatformCardWrapper}>
<div className="d-flex flex-row align-items-center">
<Icon svgPath={icon} aria-label="Close try Cody widget" className={styles.codyPlatformCardIcon} />
{typeof icon === 'string' ? (
<Icon svgPath={icon} aria-hidden={true} className={styles.codyPlatformCardIcon} />
) : (
<>{icon}</>
)}
<H3 className="ml-2 mb-0">{title}</H3>
</div>
<Text className={classNames('mt-2', styles.codyPlatformCardDescription)}>{description}</Text>

View File

@ -0,0 +1,14 @@
export const isEmailVerificationNeededForCody = (): boolean =>
window.context?.codyRequiresVerifiedEmail && !window.context?.currentUser?.hasVerifiedEmail
export const isCodyEnabled = (): boolean => {
if (window.context?.sourcegraphAppMode) {
return true
}
if (!window.context?.codyEnabled || !window.context?.codyEnabledForCurrentUser) {
return false
}
return true
}

View File

@ -11,11 +11,12 @@ import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
import { Alert, Form, Input, LoadingSpinner, Text, Badge, Link, useSessionStorage } from '@sourcegraph/wildcard'
import { BrandLogo } from '../../components/branding/BrandLogo'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { useURLSyncedString } from '../../hooks/useUrlSyncedString'
import { eventLogger } from '../../tracking/eventLogger'
import { DOTCOM_URL } from '../../tracking/util'
import { CodyIcon } from '../components/CodyIcon'
import { useIsCodyEnabled } from '../useIsCodyEnabled'
import { isEmailVerificationNeededForCody } from '../isCodyEnabled'
import { translateToQuery } from './translateToQuery'
@ -138,7 +139,6 @@ const SearchInput: React.FunctionComponent<{
onSubmit: () => void
className?: string
}> = ({ value, loading, error, onChange, onSubmit: parentOnSubmit, className }) => {
const cody = useIsCodyEnabled()
const onInput = useCallback<React.FormEventHandler<HTMLInputElement>>(
event => {
onChange(event.currentTarget.value)
@ -154,9 +154,11 @@ const SearchInput: React.FunctionComponent<{
[parentOnSubmit]
)
return cody.search ? (
const [codySearchEnabled] = useFeatureFlag('cody-web-search')
return codySearchEnabled ? (
<Form onSubmit={onSubmit} className={className}>
{cody.needsEmailVerification && (
{isEmailVerificationNeededForCody() && (
<Alert variant="warning">
<Text className="mb-0">Verify email</Text>
<Text className="mb-0">
@ -172,7 +174,7 @@ const SearchInput: React.FunctionComponent<{
inputClassName={styles.input}
value={value}
onInput={onInput}
disabled={loading || cody.needsEmailVerification}
disabled={loading || isEmailVerificationNeededForCody()}
autoFocus={true}
placeholder="Search for code or files in natural language..."
/>

View File

@ -26,7 +26,6 @@ export const CodySidebar: React.FC<CodySidebarProps> = ({ onClose }) => {
messageInProgress,
clearHistory,
loaded,
isCodyEnabled,
transcriptHistory,
deleteHistoryItem,
loadTranscriptFromHistory,
@ -76,7 +75,7 @@ export const CodySidebar: React.FC<CodySidebarProps> = ({ onClose }) => {
[loadTranscriptFromHistory, setShowHistory]
)
if (!(loaded && isCodyEnabled.sidebar)) {
if (!loaded) {
return null
}

View File

@ -55,7 +55,7 @@ export const CodySidebarStoreProvider: React.FC<ICodySidebarStoreProviderProps>
...codyChatStore,
isSidebarOpen: isSidebarOpen ?? false,
inputNeedsFocus,
sidebarSize: isSidebarOpen && codyChatStore.isCodyEnabled.sidebar ? sidebarSize : 0,
sidebarSize: isSidebarOpen ? sidebarSize : 0,
setIsSidebarOpen,
setFocusProvided,
setSidebarSize,

View File

@ -14,7 +14,7 @@ import { useLocalStorage } from '@sourcegraph/wildcard'
import { eventLogger } from '../tracking/eventLogger'
import { EventName } from '../util/constants'
import { useIsCodyEnabled, IsCodyEnabled, notEnabled } from './useIsCodyEnabled'
import { isEmailVerificationNeededForCody } from './isCodyEnabled'
export type { CodyClientScope } from '@sourcegraph/cody-shared/src/chat/useClient'
@ -38,7 +38,6 @@ export interface CodyChatStore
> {
readonly transcriptHistory: TranscriptJSON[]
readonly loaded: boolean
readonly isCodyEnabled: IsCodyEnabled
clearHistory: () => void
deleteHistoryItem: (id: string) => void
loadTranscriptFromHistory: (id: string) => Promise<void>
@ -63,7 +62,6 @@ export const codyChatStoreMock: CodyChatStore = {
setEditorScope: () => {},
transcriptHistory: [],
loaded: true,
isCodyEnabled: notEnabled,
clearHistory: () => {},
deleteHistoryItem: () => {},
loadTranscriptFromHistory: () => Promise.resolve(),
@ -94,7 +92,6 @@ export const useCodyChat = ({
onTranscriptHistoryLoad,
autoLoadTranscriptFromHistory = true,
}: CodyChatProps): CodyChatStore => {
const isCodyEnabled = useIsCodyEnabled()
const [loadedTranscriptFromHistory, setLoadedTranscriptFromHistory] = useState(false)
const [transcriptHistoryInternal, setTranscriptHistoryState] = useLocalStorage<TranscriptJSON[]>(
CODY_TRANSCRIPT_HISTORY_KEY,
@ -126,7 +123,7 @@ export const useCodyChat = ({
accessToken: null,
customHeaders: window.context.xhrHeaders,
debugEnable: false,
needsEmailVerification: isCodyEnabled.needsEmailVerification,
needsEmailVerification: isEmailVerificationNeededForCody(),
},
scope: initialScope,
onEvent,
@ -280,10 +277,7 @@ export const useCodyChat = ({
[executeRecipeInternal, updateTranscriptInHistory]
)
const loaded = useMemo(
() => loadedTranscriptFromHistory && isCodyEnabled.loaded,
[loadedTranscriptFromHistory, isCodyEnabled.loaded]
)
const loaded = useMemo(() => loadedTranscriptFromHistory, [loadedTranscriptFromHistory])
// Autoload the latest transcript from history once it is loaded. Initially the transcript is null.
useEffect(() => {
@ -373,7 +367,6 @@ export const useCodyChat = ({
return {
loaded,
isCodyEnabled,
transcript,
transcriptHistory,
chatMessages,

View File

@ -1,73 +0,0 @@
import { useMemo } from 'react'
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
export const notEnabled = {
loaded: true,
chat: false,
sidebar: false,
search: false,
editorRecipes: false,
needsEmailVerification: false,
}
export interface IsCodyEnabled {
loaded: boolean
chat: boolean
sidebar: boolean
search: boolean
editorRecipes: boolean
needsEmailVerification: boolean
}
export const isEmailVerificationNeeded = (): boolean =>
window.context?.codyRequiresVerifiedEmail && !window.context?.currentUser?.hasVerifiedEmail
export const useIsCodyEnabled = (): IsCodyEnabled => {
const [chatEnabled, chatEnabledStatus] = useFeatureFlag('cody-web-chat')
const [searchEnabled, searchEnabledStatus] = useFeatureFlag('cody-web-search')
const [sidebarEnabled, sidebarEnabledStatus] = useFeatureFlag('cody-web-sidebar')
const [editorRecipesEnabled, editorRecipesEnabledStatus] = useFeatureFlag('cody-web-editor-recipes')
let [allEnabled, allEnabledStatus] = useFeatureFlag('cody-web-all')
if (window.context?.sourcegraphAppMode) {
// If the user is using the Sourcegraph app, all features are enabled
// as long as the user has a connected Sourcegraph.com account.
allEnabled = true
}
const enabled = useMemo(
() => ({
loaded:
window.context?.sourcegraphAppMode ||
(chatEnabledStatus === 'loaded' &&
searchEnabledStatus === 'loaded' &&
sidebarEnabledStatus === 'loaded' &&
editorRecipesEnabledStatus === 'loaded' &&
allEnabledStatus === 'loaded'),
chat: chatEnabled || allEnabled,
sidebar: sidebarEnabled || allEnabled,
search: searchEnabled || allEnabled,
editorRecipes: (editorRecipesEnabled && sidebarEnabled) || allEnabled,
needsEmailVerification: isEmailVerificationNeeded(),
}),
[
chatEnabled,
sidebarEnabled,
searchEnabled,
editorRecipesEnabled,
allEnabled,
chatEnabledStatus,
searchEnabledStatus,
sidebarEnabledStatus,
editorRecipesEnabledStatus,
allEnabledStatus,
]
)
if (!window.context?.codyEnabled || !window.context?.codyEnabledForCurrentUser) {
return notEnabled
}
return enabled
}

View File

@ -28,11 +28,9 @@ export type FeatureFlagName =
| 'sourcegraph-operator-site-admin-hide-maintenance'
| 'repository-metadata'
| 'cody-gateway-management-ui'
| 'cody-web-chat'
| 'cody-web-search'
| 'cody-web-sidebar'
| 'cody-web-all'
| 'cody-web-editor-recipes'
| 'own-promote'
| 'own-analytics'
interface OrgFlagOverride {
orgID: string

View File

@ -8,7 +8,7 @@ import { useSettings } from '@sourcegraph/shared/src/settings/settings'
import { Alert, AlertProps, Markdown } from '@sourcegraph/wildcard'
import { AuthenticatedUser } from '../auth'
import { useIsCodyEnabled } from '../cody/useIsCodyEnabled'
import { isEmailVerificationNeededForCody } from '../cody/isCodyEnabled'
import { DismissibleAlert } from '../components/DismissibleAlert'
import styles from './Notices.module.scss'
@ -96,9 +96,7 @@ export const VerifyEmailNotices: React.FunctionComponent<VerifyEmailNoticesProps
alertClassName,
authenticatedUser,
}) => {
const codyEnabled = useIsCodyEnabled()
if (codyEnabled.needsEmailVerification && authenticatedUser) {
if (isEmailVerificationNeededForCody() && authenticatedUser) {
return (
<div className={classNames(styles.notices, className)}>
<NoticeAlert

View File

@ -424,7 +424,8 @@ describe('Repository', () => {
// Assert that the directory listing displays properly
await driver.page.waitForSelector('.test-tree-entries')
await percySnapshotWithVariants(driver.page, 'Repository index page')
// TODO: Reenable later, percy is erroring out on remote images not loading.
// await percySnapshotWithVariants(driver.page, 'Repository index page')
await accessibilityAudit(driver.page)
const numberOfFileEntries = await driver.page.evaluate(

View File

@ -10,7 +10,7 @@ import { Checkbox, Form, H3, Modal, Text, Button, Icon, useLocalStorage } from '
import { AuthenticatedUser } from '../../auth'
import { CodyPageIcon } from '../../cody/chat/CodyPageIcon'
import { useIsCodyEnabled } from '../../cody/useIsCodyEnabled'
import { isEmailVerificationNeededForCody } from '../../cody/isCodyEnabled'
import { LoaderButton } from '../../components/LoaderButton'
import { SubmitCodySurveyResult, SubmitCodySurveyVariables } from '../../graphql-operations'
import { resendVerificationEmail } from '../../user/settings/emails/UserEmail'
@ -195,8 +195,7 @@ export const CodySurveyToast: React.FC<
} & TelemetryProps
> = ({ authenticatedUser, telemetryService }) => {
const { show, dismiss } = useCodySurveyToast()
const codyEnabled = useIsCodyEnabled()
const [showVerifyEmail, setShowVerifyEmail] = useState(show && codyEnabled.needsEmailVerification)
const [showVerifyEmail, setShowVerifyEmail] = useState(show && isEmailVerificationNeededForCody())
const dismissVerifyEmail = useCallback(() => {
telemetryService.log('VerifyEmailToastDismissed')
setShowVerifyEmail(false)

View File

@ -24,7 +24,6 @@ import { BatchChangesNavItem } from '../batches/BatchChangesNavItem'
import { CodeMonitoringLogo } from '../code-monitoring/CodeMonitoringLogo'
import { CodeMonitoringProps } from '../codeMonitoring'
import { CodyLogo } from '../cody/components/CodyLogo'
import { useIsCodyEnabled } from '../cody/useIsCodyEnabled'
import { BrandLogo } from '../components/branding/BrandLogo'
import { useFuzzyFinderFeatureFlags } from '../components/fuzzyFinder/FuzzyFinderFeatureFlag'
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
@ -150,7 +149,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
const showSearchNotebook = notebooksEnabled && !isSourcegraphApp
const isLicensed = !!window.context?.licenseInfo || isSourcegraphApp // Assume licensed when running as a native app
const showBatchChanges = ((props.batchChangesEnabled && isLicensed) || isSourcegraphDotCom) && !isSourcegraphApp // Batch changes are enabled on sourcegraph.com so users can see the Getting Started page
const codyEnabled = useIsCodyEnabled()
const [codySearchEnabled] = useFeatureFlag('cody-web-search')
const [isSentinelEnabled] = useFeatureFlag('sentinel')
// TODO: Include isSourcegraphDotCom in subsequent PR
@ -177,7 +176,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
const items: (NavDropdownItem | false)[] = [
!!showSearchContext && { path: EnterprisePageRoutes.Contexts, content: 'Contexts' },
ownEnabled && { path: EnterprisePageRoutes.Own, content: 'Own' },
codyEnabled.search && {
codySearchEnabled && {
path: EnterprisePageRoutes.CodySearch,
content: (
<>
@ -187,7 +186,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
},
]
return items.filter<NavDropdownItem>((item): item is NavDropdownItem => !!item)
}, [ownEnabled, showSearchContext, codyEnabled.search])
}, [ownEnabled, showSearchContext, codySearchEnabled])
const { fuzzyFinderNavbar } = useFuzzyFinderFeatureFlags()
@ -229,13 +228,11 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
</NavLink>
</NavItem>
))}
{(codyEnabled.chat || isSourcegraphDotCom) && (
<NavItem icon={CodyLogo}>
<NavLink variant={navLinkVariant} to={EnterprisePageRoutes.Cody}>
Cody AI
</NavLink>
</NavItem>
)}
<NavItem icon={CodyLogo}>
<NavLink variant={navLinkVariant} to={EnterprisePageRoutes.Cody}>
Cody AI
</NavLink>
</NavItem>
{showSearchNotebook && (
<NavItem icon={BookOutlineIcon}>
<NavLink variant={navLinkVariant} to={EnterprisePageRoutes.Notebooks}>

View File

@ -84,6 +84,47 @@ exports[`GlobalNavbar default 1`] = `
</button>
</div>
</li>
<li
class="item"
>
<a
class="link"
href="/cody"
>
<span
class="linkContent"
>
<svg
aria-hidden="true"
class="mdi-icon iconInline icon"
fill="none"
height="20"
role="img"
viewBox="0 0 20 20"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9088 4C14.756 4 15.4429 4.69836 15.4429 5.55983V8.33286C15.4429 9.19433 14.756 9.89269 13.9088 9.89269C13.0615 9.89269 12.3747 9.19433 12.3747 8.33286V5.55983C12.3747 4.69836 13.0615 4 13.9088 4Z"
fill="#a6b6d9"
/>
<path
d="M4.19287 7.63942C4.19287 6.77795 4.87971 6.07959 5.72696 6.07959H8.45423C9.30148 6.07959 9.98832 6.77795 9.98832 7.63942C9.98832 8.50089 9.30148 9.19925 8.45423 9.19925H5.72696C4.87971 9.19925 4.19287 8.50089 4.19287 7.63942Z"
fill="#a6b6d9"
/>
<path
d="M17.5756 12.1801C18.1216 12.7075 18.1437 13.5851 17.625 14.1403L17.1423 14.6569C13.3654 18.6994 6.99777 18.5987 3.34628 14.4387C2.84481 13.8674 2.89377 12.9909 3.45565 12.481C4.01752 11.9711 4.87954 12.0209 5.38102 12.5922C7.97062 15.5424 12.4865 15.6139 15.1651 12.747L15.6477 12.2304C16.1664 11.6752 17.0296 11.6527 17.5756 12.1801Z"
fill="#a6b6d9"
/>
</svg>
<span
class="text iconIncluded"
>
Cody AI
</span>
</span>
</a>
</li>
<li
class="item"
>

View File

@ -155,7 +155,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
)
const {
isCodyEnabled,
sidebarSize: codySidebarSize,
isSidebarOpen: isCodySidebarOpen,
setIsSidebarOpen: setIsCodySidebarOpen,
@ -359,16 +358,15 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
return (
<>
{isCodyEnabled.sidebar &&
focusCodyShortcut?.keybindings.map((keybinding, index) => (
<Shortcut
key={index}
{...keybinding}
onMatch={() => {
setIsCodySidebarOpen(true)
}}
/>
))}
{focusCodyShortcut?.keybindings.map((keybinding, index) => (
<Shortcut
key={index}
{...keybinding}
onMatch={() => {
setIsCodySidebarOpen(true)
}}
/>
))}
<div className={classNames('w-100 d-flex flex-row')}>
<div className={classNames('w-100 d-flex flex-column', styles.repoContainer)}>
<RepoHeader
@ -383,28 +381,26 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
telemetryService={props.telemetryService}
/>
{isCodyEnabled.sidebar ? (
<RepoHeaderContributionPortal
position="right"
priority={1}
id="cody"
{...repoHeaderContributionsLifecycleProps}
>
{() =>
!isCodySidebarOpen ? (
<AskCodyButton
onClick={() => {
props.telemetryService.log(EventName.CODY_SIDEBAR_CHAT_OPENED, {
repo,
path: filePath,
})
setIsCodySidebarOpen(true)
}}
/>
) : null
}
</RepoHeaderContributionPortal>
) : null}
<RepoHeaderContributionPortal
position="right"
priority={1}
id="cody"
{...repoHeaderContributionsLifecycleProps}
>
{() =>
!isCodySidebarOpen ? (
<AskCodyButton
onClick={() => {
props.telemetryService.log(EventName.CODY_SIDEBAR_CHAT_OPENED, {
repo,
path: filePath,
})
setIsCodySidebarOpen(true)
}}
/>
) : null
}
</RepoHeaderContributionPortal>
<RepoHeaderContributionPortal
position="right"
@ -499,7 +495,7 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
</Suspense>
</div>
{isCodyEnabled.sidebar && isCodySidebarOpen && (
{isCodySidebarOpen && (
<Panel
className="cody-sidebar-panel"
position="right"

View File

@ -46,6 +46,7 @@ import {
import { AuthenticatedUser } from '../../auth'
import { CodeIntelligenceProps } from '../../codeintel'
import { FileContentEditor } from '../../cody/components/FileContentEditor'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import { useCodySidebar } from '../../cody/sidebar/Provider'
import { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { HeroPage } from '../../components/HeroPage'
@ -362,12 +363,13 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
const alwaysRender = (
<>
<PageTitle title={getPageTitle()} />
{props.isSourcegraphDotCom && (
{(props.isSourcegraphDotCom || isCodyEnabled()) && (
<TryCodyWidget
telemetryService={props.telemetryService}
type="blob"
authenticatedUser={props.authenticatedUser}
context={context}
isSourcegraphDotCom={props.isSourcegraphDotCom}
/>
)}
{window.context.isAuthenticatedUser && (

View File

@ -28,6 +28,7 @@ import { AbsoluteRepoFile, ModeSpec, parseQueryAndHash } from '@sourcegraph/shar
import { useLocalStorage } from '@sourcegraph/wildcard'
import { CodeMirrorEditor } from '../../cody/components/CodeMirrorEditor'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import { useCodySidebar } from '../../cody/sidebar/Provider'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { ExternalLinkFields, Scalars } from '../../graphql-operations'
@ -275,7 +276,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
)
// Added fallback to take care of ReferencesPanel/Simple storybook
const { isCodyEnabled, setEditorScope } = useCodySidebar()
const { setEditorScope } = useCodySidebar()
const editorRef = useRef<EditorView | null>(null)
@ -295,7 +296,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
}),
scipSnapshot(blobInfo.snapshotData),
codeFoldingExtension(),
isCodyEnabled.editorRecipes
isCodyEnabled()
? codyWidgetExtension(
editorRef.current
? new CodeMirrorEditor({

View File

@ -36,7 +36,7 @@
}
.card {
padding-top: 2.9375rem;
padding-top: 0.5rem;
gap: 1rem;
}
@ -45,7 +45,6 @@
font-weight: 600;
font-size: 1.25rem;
line-height: 1.5rem;
letter-spacing: -0.25px;
margin-bottom: 0.6875rem;
}

View File

@ -27,6 +27,7 @@ const AUTO_DISMISS_ON_EVENTS = new Set([
interface WidgetContentProps extends TelemetryProps {
type: 'blob' | 'repo'
theme?: 'light' | 'dark'
isSourcegraphDotCom: boolean
}
interface NoAuhWidgetContentProps extends WidgetContentProps {
@ -77,7 +78,7 @@ const NoAuthWidgetContent: React.FC<NoAuhWidgetContentProps> = ({ type, telemetr
return (
<>
<MeetCodySVG />
<div>
<div className="flex-grow-1">
<H2 className={styles.cardTitle}>{title}</H2>
<Text className={styles.cardDescription}>
Cody combines an LLM with the context of Sourcegraph's code graph on public code or your code at
@ -86,8 +87,8 @@ const NoAuthWidgetContent: React.FC<NoAuhWidgetContentProps> = ({ type, telemetr
<div className={styles.authButtonsWrap}>
<ExternalsAuth
context={context}
githubLabel="Github"
gitlabLabel="Gitlab"
githubLabel="GitHub"
gitlabLabel="GitLab"
withCenteredText={true}
onClick={logEvent}
ctaClassName={styles.authButton}
@ -102,7 +103,7 @@ const NoAuthWidgetContent: React.FC<NoAuhWidgetContentProps> = ({ type, telemetr
Email
</Link>
</div>
<Text className="mb-0 mt-2">
<Text className="mb-2 mt-2">
By registering, you agree to our{' '}
<Link
to="https://about.sourcegraph.com/terms"
@ -127,9 +128,9 @@ const NoAuthWidgetContent: React.FC<NoAuhWidgetContentProps> = ({ type, telemetr
)
}
const AuthUserWidgetContent: React.FC<WidgetContentProps> = ({ type, theme }) => {
const { title, useCases, image } =
type === 'blob'
const AuthUserWidgetContent: React.FC<WidgetContentProps> = ({ type, theme, isSourcegraphDotCom }) => {
const { title, useCases, image } = isSourcegraphDotCom
? type === 'blob'
? {
title: 'Try Cody on public code',
useCases: ['Select code in the file below', 'Select an action with Cody widget'],
@ -143,14 +144,30 @@ const AuthUserWidgetContent: React.FC<WidgetContentProps> = ({ type, theme }) =>
],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-chat-banner-image-${theme}.png`,
}
: type === 'blob'
? {
title: 'Try Cody AI assist on this file',
useCases: ['Select code in the file below', 'Select an action with Cody widget'],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-action-bar-${theme}.png`,
}
: {
title: 'Try Cody AI assist on this repo',
useCases: [
'Click the Ask Cody button above and to the right of this banner',
'Ask Cody a question like “Explain the structure of this repository”',
],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-chat-banner-image-${theme}.png`,
}
return (
<>
<div className="d-flex pb-3">
<GlowingCodySVG />
<div className="d-flex flex-column flex-grow-1 justify-content-center">
<H4 className={styles.cardTitle}>{title}</H4>
<ol className={classNames('m-0 pl-4 fs-6', styles.cardList)}>
<div className="d-flex flex-column flex-grow-1 justify-content-center flex-shrink-0">
<H4 as="h2" className={styles.cardTitle}>
{title}
</H4>
<ol className={classNames('m-0 pl-4', styles.cardList)}>
{useCases.map(useCase => (
<Text key={useCase} as="li">
{useCase}
@ -160,7 +177,7 @@ const AuthUserWidgetContent: React.FC<WidgetContentProps> = ({ type, theme }) =>
</div>
</div>
<div className={classNames('d-flex justify-content-center', styles.cardImages)}>
<img src={image} alt="Cody" className={styles.cardImage} />
<img src={image} alt="Cody" className={classNames(styles.cardImage, 'percy-hide')} />
</div>
</>
)
@ -171,6 +188,7 @@ interface TryCodyWidgetProps extends TelemetryProps {
type: 'blob' | 'repo'
authenticatedUser: AuthenticatedUser | null
context: Pick<SourcegraphContext, 'authProviders'>
isSourcegraphDotCom: boolean
}
export const TryCodyWidget: React.FC<TryCodyWidgetProps> = ({
@ -179,6 +197,7 @@ export const TryCodyWidget: React.FC<TryCodyWidgetProps> = ({
authenticatedUser,
context,
type,
isSourcegraphDotCom,
}) => {
const isLightTheme = useIsLightTheme()
const { isDismissed, onDismiss } = useTryCodyWidget(telemetryService)
@ -196,7 +215,11 @@ export const TryCodyWidget: React.FC<TryCodyWidgetProps> = ({
return (
<MarketingBlock
wrapperClassName={classNames(className, type === 'blob' ? styles.blobCardWrapper : styles.repoCardWrapper)}
wrapperClassName={classNames(
className,
type === 'blob' ? styles.blobCardWrapper : styles.repoCardWrapper,
'mb-2'
)}
contentClassName={classNames(
'd-flex position-relative pb-0 overflow-auto justify-content-between',
styles.card,
@ -209,9 +232,15 @@ export const TryCodyWidget: React.FC<TryCodyWidgetProps> = ({
type={type}
theme={isLightTheme ? 'light' : 'dark'}
telemetryService={telemetryService}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
) : (
<NoAuthWidgetContent telemetryService={telemetryService} type={type} context={context} />
<NoAuthWidgetContent
telemetryService={telemetryService}
type={type}
context={context}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
)}
<Button className={classNames(styles.closeButton, 'position-absolute mt-2')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />

View File

@ -99,6 +99,9 @@ describe('TreePage', () => {
it('Should displays cody CTA', () => {
const repo = repoDefaults()
const props = treePagePropsDefaults(repo)
window.context = window.context || {}
window.context.codyEnabled = true
window.context.codyEnabledForCurrentUser = true
const mockUser = {
id: 'userID',

View File

@ -46,6 +46,7 @@ import { AuthenticatedUser } from '../../auth'
import { BatchChangesProps } from '../../batches'
import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton'
import { CodeIntelligenceProps } from '../../codeintel'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { PageTitle } from '../../components/PageTitle'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
@ -361,13 +362,14 @@ export const TreePage: FC<Props> = ({
return (
<div className={classNames(styles.treePage, className)}>
{isSourcegraphDotCom && (
{(isSourcegraphDotCom || isCodyEnabled()) && (
<TryCodyWidget
className="mb-2"
telemetryService={props.telemetryService}
type="repo"
authenticatedUser={authenticatedUser}
context={context}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
)}
<Container className={styles.container}>

View File

@ -5,5 +5,3 @@ export const isPackagesEnabled = (): boolean =>
window.context?.experimentalFeatures?.rubyPackages === 'enabled' ||
window.context?.experimentalFeatures?.pythonPackages === 'enabled' ||
window.context?.experimentalFeatures?.rustPackages === 'enabled'
export const isCodyEnabled = (): boolean => window.context?.codyEnabled

View File

@ -86,17 +86,14 @@ export const SearchPageContent: FC<SearchPageContentProps> = props => {
) : (
<>
<SearchPageInput queryState={queryState} setQueryState={setQueryState} />
{isSourcegraphDotCom && (
<>
{authenticatedUser ? (
<TryCodyCtaSection className="mx-auto my-5" telemetryService={telemetryService} />
) : (
<TryCodySignUpCtaSection
className="mx-auto my-5"
telemetryService={telemetryService}
/>
)}
</>
{authenticatedUser ? (
<TryCodyCtaSection
className="mx-auto my-5"
telemetryService={telemetryService}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
) : (
<TryCodySignUpCtaSection className="mx-auto my-5" telemetryService={telemetryService} />
)}
</>
)}

View File

@ -23,10 +23,6 @@
background-color: var(--color-bg-1);
}
.vscode-icon {
color: var(--primary);
}
.try-cody-link {
font-size: 1rem;
font-weight: 600;
@ -37,3 +33,11 @@
top: 0;
right: 0;
}
.not-enabled-block {
margin: 0 auto;
position: relative;
max-width: 720px;
padding: 2rem;
padding-top: 0;
}

View File

@ -1,12 +1,13 @@
import React from 'react'
import { mdiArrowRight, mdiClose, mdiMicrosoftVisualStudioCode } from '@mdi/js'
import { mdiArrowRight, mdiClose } from '@mdi/js'
import classNames from 'classnames'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Button, ButtonLink, H2, H3, Icon, Link, Text } from '@sourcegraph/wildcard'
import { isCodyEnabled } from '../../../cody/isCodyEnabled'
import { MarketingBlock } from '../../../components/MarketingBlock'
import { EventName } from '../../../util/constants'
@ -67,22 +68,46 @@ const MeetCodySVG: React.FC = () => (
</svg>
)
export const TryCodyCtaSection: React.FC<TelemetryProps & { className?: string }> = ({
interface TryCodyCtaSectionProps extends TelemetryProps {
isSourcegraphDotCom: boolean
className?: string
}
export const TryCodyCtaSection: React.FC<TryCodyCtaSectionProps> = ({
className,
telemetryService,
isSourcegraphDotCom,
}) => {
const [isDismissed = true, setIsDismissed] = useTemporarySetting('cody.searchPageCta.dismissed', false)
const onDismiss = (): void => setIsDismissed(true)
const logEvent = (eventName: EventName): void =>
telemetryService.log(eventName, { type: 'ComHome' }, { type: 'ComHome' })
const onInstallClick = (): void => logEvent(EventName.TRY_CODY_VSCODE)
const onMarketplaceClick = (): void => logEvent(EventName.TRY_CODY_MARKETPLACE)
const onViewEditorExtensionsClick = (): void => logEvent(EventName.VIEW_EDITOR_EXTENSIONS)
const onTryWebClick = (): void => logEvent(EventName.TRY_CODY_WEB)
if (isDismissed) {
return null
}
if (!isCodyEnabled()) {
return (
<div
className={classNames(styles.notEnabledBlock, 'd-flex justify-content-center align-items-center mt-5')}
>
<div className={classNames(styles.codyIllustration)}>
<MeetCodySVG />
</div>
<Text>
<Link to="https://about.sourcegraph.com/cody?utm_source=server">Learn about Cody</Link>,
Sourcegraph's AI coding assistant
</Text>
<Button className={classNames(styles.closeButton, 'position-absolute m-0')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />
</Button>
</div>
)
}
return (
<div className={classNames('d-flex position-relative pt-4', className, styles.container)}>
<div className={classNames(styles.codyIllustration)}>
@ -94,67 +119,111 @@ export const TryCodyCtaSection: React.FC<TelemetryProps & { className?: string }
contentClassName={classNames('flex-grow-1 d-flex flex-column justify-content-between p-4', styles.card)}
>
<H3 className="d-flex align-items-center">
<Icon
svgPath={mdiMicrosoftVisualStudioCode}
aria-hidden={true}
className={classNames(styles.vscodeIcon, 'mr-1')}
size="md"
/>
Install Cody for VS Code
<CodyInIDEIcon aria-hidden={true} />
Install Cody for your IDE
</H3>
<Text>
Cody for VS Code provides the power of LLMs to help you generate and fix code, right where you
Cody for your IDE provides the power of LLMs to help you generate and fix code, right where you
commit.
</Text>
<div className="mb-2">
<ButtonLink
to="vscode:extension/sourcegraph.cody-ai"
variant="merged"
to="/help/cody#get-cody"
variant="primary"
className="d-inline-flex align-items-center"
onClick={onInstallClick}
onClick={onViewEditorExtensionsClick}
>
Install Cody for VS Code <Icon svgPath={mdiArrowRight} aria-hidden={true} size="md" />
View editor extensions <Icon svgPath={mdiArrowRight} aria-hidden={true} size="md" />
</ButtonLink>
</div>
<Text
size="small"
as={Link}
to="https://marketplace.visualstudio.com/items?itemName=sourcegraph.cody-ai"
target="_blank"
rel="noopener"
onClick={onMarketplaceClick}
>
or download on the VS Code marketplace
</Text>
</MarketingBlock>
<div className="d-flex flex-column justify-content-center p-4">
<H3>Cody for Sourcegraph.com</H3>
<Text>
A free, helpful AI assistant, that explains, generates, and transpiles code, in the Sourcegraph web
interface.
</Text>
<Text
as={Link}
to="/cody"
className={classNames('d-flex align-items-center mb-2', styles.tryCodyLink)}
onClick={onTryWebClick}
>
Try Cody chat
<Icon svgPath={mdiArrowRight} aria-hidden={true} size="sm" className="ml-1" />
</Text>
<Text
as={Link}
to="https://sourcegraph.com/github.com/openai/openai-cookbook/-/blob/apps/file-q-and-a/nextjs-with-flask-server/server/answer_question.py"
className={classNames('d-flex align-items-center', styles.tryCodyLink)}
onClick={onTryWebClick}
>
Try Cody on a file
<Icon svgPath={mdiArrowRight} aria-hidden={true} size="sm" className="ml-1" />
</Text>
</div>
<Button className={classNames(styles.closeButton, 'position-absolute m-0')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />
</Button>
{isSourcegraphDotCom ? (
<>
<div className="d-flex flex-column justify-content-center p-4">
<H3>Cody for Sourcegraph.com</H3>
<Text>
A free, helpful AI assistant, that explains, generates, and transpiles code, in the
Sourcegraph web interface.
</Text>
<Text
as={Link}
to="/cody"
className={classNames('d-flex align-items-center mb-2', styles.tryCodyLink)}
onClick={onTryWebClick}
>
Try Cody chat
<Icon svgPath={mdiArrowRight} aria-hidden={true} size="sm" className="ml-1" />
</Text>
<Text
as={Link}
to="https://sourcegraph.com/github.com/openai/openai-cookbook/-/blob/apps/file-q-and-a/nextjs-with-flask-server/server/answer_question.py"
className={classNames('d-flex align-items-center', styles.tryCodyLink)}
onClick={onTryWebClick}
>
Try Cody on a file
<Icon svgPath={mdiArrowRight} aria-hidden={true} size="sm" className="ml-1" />
</Text>
</div>
<Button className={classNames(styles.closeButton, 'position-absolute m-0')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />
</Button>
</>
) : (
<>
<div className="d-flex flex-column justify-content-center p-4">
<H3>Try Cody AI in the web application</H3>
<Text>
Cody for Sourcegraph explains, generates, and translates code, right in the web interface.
</Text>
<Text
as={Link}
to="/cody"
className={classNames('d-flex align-items-center mb-2', styles.tryCodyLink)}
onClick={onTryWebClick}
>
Cody Chat
<Icon svgPath={mdiArrowRight} aria-hidden={true} size="sm" className="ml-1" />
</Text>
</div>
<Button className={classNames(styles.closeButton, 'position-absolute m-0')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />
</Button>
</>
)}
</div>
)
}
const CodyInIDEIcon: React.FunctionComponent<{ className?: string }> = ({ className }) => (
<svg
width="34"
height="23"
viewBox="10 0 48 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M17.5623 26.079C17.5623 26.4422 17.7005 26.7906 17.9467 27.0474C18.1928 27.3042 18.5267 27.4485 18.8748 27.4485H21.4998V30.1877H18.2185C17.4966 30.1877 16.2498 29.5714 16.2498 28.8181C16.2498 29.5714 15.0029 30.1877 14.281 30.1877H10.9998V27.4485H13.6248C13.9729 27.4485 14.3067 27.3042 14.5528 27.0474C14.799 26.7906 14.9373 26.4422 14.9373 26.079V6.90505C14.9373 6.54182 14.799 6.19347 14.5528 5.93662C14.3067 5.67978 13.9729 5.53549 13.6248 5.53549H10.9998V2.79636H14.281C15.0029 2.79636 16.2498 3.41266 16.2498 4.16592C16.2498 3.41266 17.4966 2.79636 18.2185 2.79636H21.4998V5.53549H18.8748C18.5267 5.53549 18.1928 5.67978 17.9467 5.93662C17.7005 6.19347 17.5623 6.54182 17.5623 6.90505V26.079Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M39.5458 7.992C40.6755 7.992 41.5912 8.86333 41.5912 9.93817V13.398C41.5912 14.4729 40.6755 15.3442 39.5458 15.3442C38.4161 15.3442 37.5003 14.4729 37.5003 13.398V9.93817C37.5003 8.86333 38.4161 7.992 39.5458 7.992Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26.5908 12.533C26.5908 11.4581 27.5066 10.5868 28.6362 10.5868H32.2726C33.4023 10.5868 34.3181 11.4581 34.3181 12.533C34.3181 13.6078 33.4023 14.4791 32.2726 14.4791H28.6362C27.5066 14.4791 26.5908 13.6078 26.5908 12.533Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M44.4341 18.1987C45.1621 18.8567 45.1916 19.9517 44.5 20.6444L43.8564 21.2889C38.8205 26.3326 30.3304 26.207 25.4617 21.0167C24.7931 20.3039 24.8584 19.2103 25.6075 18.5741C26.3567 17.938 27.5061 18.0001 28.1747 18.7129C31.6275 22.3938 37.6486 22.4829 41.2201 18.906L41.8636 18.2614C42.5552 17.5687 43.7061 17.5407 44.4341 18.1987Z"
fill="#A305E1"
/>
</svg>
)

View File

@ -20,6 +20,7 @@ export const enum EventName {
CODY_SIGNUP = 'CodySignup',
CODY_CHAT_DOWNLOAD_VSCODE = 'web:codyChat:downloadVSCode',
CODY_CHAT_TRY_ON_PUBLIC_CODE = 'web:codyChat:tryOnPublicCode',
VIEW_EDITOR_EXTENSIONS = 'CodyClickViewEditorExtensions',
TRY_CODY_VSCODE = 'VSCodeInstall',
TRY_CODY_MARKETPLACE = 'VSCodeMarketplace',
TRY_CODY_WEB = 'TryCodyWeb',