diff --git a/client/cody-ui/BUILD.bazel b/client/cody-ui/BUILD.bazel index 2af052230a1..47ac3b81cca 100644 --- a/client/cody-ui/BUILD.bazel +++ b/client/cody-ui/BUILD.bazel @@ -27,6 +27,10 @@ ts_project( "src/Chat.tsx", "src/Terms.tsx", "src/Tips.tsx", + "src/chat/ChatMessageLoading.tsx", + "src/chat/ChatMessageRow.tsx", + "src/chat/ChatMessages.story.tsx", + "src/chat/ChatMessages.tsx", "src/chat/CodeBlocks.tsx", "src/chat/ContextFiles.tsx", "src/globals.d.ts", @@ -43,6 +47,7 @@ ts_project( deps = [ ":node_modules/@sourcegraph/cody-shared", "//:node_modules/@mdi/js", + "//:node_modules/@storybook/react", #keep "//:node_modules/@types/classnames", "//:node_modules/@types/react", "//:node_modules/classnames", diff --git a/client/cody-ui/package.json b/client/cody-ui/package.json index 945cd1cc9cd..cdcfca227b7 100644 --- a/client/cody-ui/package.json +++ b/client/cody-ui/package.json @@ -15,7 +15,8 @@ "build": "tsc -b", "lint": "pnpm run lint:js", "lint:js": "eslint --cache '**/*.[tj]s?(x)'", - "test": "jest" + "test": "jest", + "storybook": "STORIES_GLOB='client/cody-ui/src/**/*.story.tsx' pnpm --filter @sourcegraph/storybook run start" }, "dependencies": { "@sourcegraph/cody-shared": "workspace:*" diff --git a/client/cody-ui/src/Chat.module.css b/client/cody-ui/src/Chat.module.css index e4ae93304c8..da8d8a3460c 100644 --- a/client/cody-ui/src/Chat.module.css +++ b/client/cody-ui/src/Chat.module.css @@ -1,7 +1,3 @@ -:root { - --chat-bubble-border-radius: 16px; -} - .inner-container { display: flex; flex-direction: column; @@ -12,102 +8,6 @@ flex: 1; } -.bubble-container { - padding: 0; -} - -.bubble-row { - display: flex; - padding: 1rem; -} - -.human-bubble-row { - justify-content: end; -} - -.bot-bubble-row { - justify-content: start; -} - -.bubble { - min-width: 0; - max-width: min(93%, 800px); -} - -.bubble-content { - padding: 1rem; - border-top-right-radius: var(--chat-bubble-border-radius); - border-top-left-radius: var(--chat-bubble-border-radius); - word-break: break-word; - line-height: 150%; -} - -.bubble-content *:first-child { - margin-top: 0 !important; -} - -.bubble-content *:last-child { - margin-bottom: 0 !important; -} - -.bubble-content pre { - padding: 0.5rem; - border-radius: 0.5rem; - overflow-x: auto; -} - -.human-bubble-content { - border-bottom-right-radius: 0; - border-bottom-left-radius: var(--chat-bubble-border-radius); -} - -.bot-bubble-content { - border-bottom-right-radius: var(--chat-bubble-border-radius); - border-bottom-left-radius: 0; -} - -.bubble-footer { - display: flex; - margin-top: 0.5rem; - align-content: center; - justify-content: space-between; -} - -.human-bubble-footer { - justify-content: flex-end; -} - -.bubble-footer-timestamp { - margin-top: 0.5rem; - align-self: center; -} - -@keyframes blink { - 50% { - background-color: transparent; - } -} - -.bubble-loader { - display: flex; - justify-content: space-between; -} - -.bubble-loader-dot { - animation: 1s blink infinite; - width: 0.5rem; - height: 0.5rem; - border-radius: 0.5rem; -} - -.bubble-loader-dot:nth-child(2) { - animation-delay: 250ms; -} - -.bubble-loader-dot:nth-child(3) { - animation-delay: 500ms; -} - .input-row { display: flex; padding: 1rem; diff --git a/client/cody-ui/src/Chat.tsx b/client/cody-ui/src/Chat.tsx index d44372d4dc4..9d7de174f81 100644 --- a/client/cody-ui/src/Chat.tsx +++ b/client/cody-ui/src/Chat.tsx @@ -2,11 +2,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import classNames from 'classnames' -import { renderMarkdown } from '@sourcegraph/cody-shared/src/chat/markdown' import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' -import { CodeBlocks } from './chat/CodeBlocks' -import { ContextFiles, FileLinkProps } from './chat/ContextFiles' +import { ChatMessages, ChatMessagesClassNames } from './chat/ChatMessages' +import { FileLinkProps } from './chat/ContextFiles' import { Tips } from './Tips' import styles from './Chat.module.css' @@ -28,16 +27,8 @@ interface ChatProps extends ChatClassNames { className?: string } -interface ChatClassNames { +interface ChatClassNames extends ChatMessagesClassNames { transcriptContainerClassName?: string - bubbleContentClassName?: string - bubbleClassName?: string - bubbleRowClassName?: string - humanBubbleContentClassName?: string - botBubbleContentClassName?: string - codeBlocksCopyButtonClassName?: string - bubbleFooterClassName?: string - bubbleLoaderDotClassName?: string inputRowClassName?: string chatInputClassName?: string } @@ -141,8 +132,6 @@ export const Chat: React.FunctionComponent = ({ [inputHistory, onChatSubmit, formInput, historyIndex, setFormInput] ) - const getBubbleClassName = (speaker: string): string => (speaker === 'human' ? 'human' : 'bot') - useEffect(() => { if (transcriptContainerRef.current) { // Only scroll if the user didn't scroll up manually more than the scrolling threshold. @@ -172,103 +161,19 @@ export const Chat: React.FunctionComponent = ({ )} {transcript.length > 0 && ( -
- {transcript.map((message, index) => ( -
-
-
- {message.displayText && ( - - )} - {message.contextFiles && message.contextFiles.length > 0 && ( - - )} -
-
-
{`${ - message.speaker === 'assistant' ? 'Cody' : 'Me' - } · ${message.timestamp}`}
-
-
-
- ))} - - {messageInProgress && messageInProgress.speaker === 'assistant' && ( -
-
-
- {messageInProgress.displayText ? ( -

- ) : ( -

-
-
-
-
- )} -
-
- Cody is typing... -
-
-
- )} -
+ )}
diff --git a/client/cody-ui/src/chat/ChatMessageLoading.module.css b/client/cody-ui/src/chat/ChatMessageLoading.module.css new file mode 100644 index 00000000000..626c59f37f1 --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessageLoading.module.css @@ -0,0 +1,25 @@ +@keyframes blink { + 50% { + background-color: transparent; + } +} + +.bubble-loader { + display: flex; + justify-content: space-between; +} + +.bubble-loader-dot { + animation: 1s blink infinite; + width: 0.5rem; + height: 0.5rem; + border-radius: 0.5rem; +} + +.bubble-loader-dot:nth-child(2) { + animation-delay: 250ms; +} + +.bubble-loader-dot:nth-child(3) { + animation-delay: 500ms; +} diff --git a/client/cody-ui/src/chat/ChatMessageLoading.tsx b/client/cody-ui/src/chat/ChatMessageLoading.tsx new file mode 100644 index 00000000000..9312c1a1d04 --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessageLoading.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +import classNames from 'classnames' + +import styles from './ChatMessageLoading.module.css' + +export const ChatMessageLoading: React.FunctionComponent<{ bubbleLoaderDotClassName?: string }> = ({ + bubbleLoaderDotClassName, +}) => ( +
+
+
+
+
+) diff --git a/client/cody-ui/src/chat/ChatMessageRow.module.css b/client/cody-ui/src/chat/ChatMessageRow.module.css new file mode 100644 index 00000000000..05efa55e13f --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessageRow.module.css @@ -0,0 +1,69 @@ +:root { + --chat-bubble-border-radius: 16px; +} + +.bubble-row { + display: flex; + padding: 1rem; +} + +.human-bubble-row { + justify-content: end; +} + +.bot-bubble-row { + justify-content: start; +} + +.bubble { + min-width: 0; + max-width: min(93%, 800px); +} + +.bubble-content { + padding: 1rem; + border-top-right-radius: var(--chat-bubble-border-radius); + border-top-left-radius: var(--chat-bubble-border-radius); + word-break: break-word; + line-height: 150%; +} + +.bubble-content *:first-child { + margin-top: 0 !important; +} + +.bubble-content *:last-child { + margin-bottom: 0 !important; +} + +.bubble-content pre { + padding: 0.5rem; + border-radius: 0.5rem; + overflow-x: auto; +} + +.human-bubble-content { + border-bottom-right-radius: 0; + border-bottom-left-radius: var(--chat-bubble-border-radius); +} + +.bot-bubble-content { + border-bottom-right-radius: var(--chat-bubble-border-radius); + border-bottom-left-radius: 0; +} + +.bubble-footer { + display: flex; + margin-top: 0.5rem; + align-content: center; + justify-content: space-between; +} + +.human-bubble-footer { + justify-content: flex-end; +} + +.bubble-footer-timestamp { + margin-top: 0.5rem; + align-self: center; +} diff --git a/client/cody-ui/src/chat/ChatMessageRow.tsx b/client/cody-ui/src/chat/ChatMessageRow.tsx new file mode 100644 index 00000000000..15367228c8b --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessageRow.tsx @@ -0,0 +1,99 @@ +import React from 'react' + +import classNames from 'classnames' + +import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' + +import { ChatMessageLoading } from './ChatMessageLoading' +import { CodeBlocks } from './CodeBlocks' +import { ContextFiles, FileLinkProps } from './ContextFiles' + +import styles from './ChatMessageRow.module.css' + +export interface ChatMessageRowClassNames { + bubbleContentClassName?: string + bubbleClassName?: string + bubbleRowClassName?: string + humanBubbleContentClassName?: string + botBubbleContentClassName?: string + codeBlocksCopyButtonClassName?: string + bubbleFooterClassName?: string + bubbleLoaderDotClassName?: string +} + +export const ChatMessageRow: React.FunctionComponent< + { + message: ChatMessage + inProgress: boolean + fileLinkComponent: React.FunctionComponent + className?: string + } & ChatMessageRowClassNames +> = ({ + message, + inProgress, + fileLinkComponent, + className, + bubbleContentClassName, + bubbleClassName, + bubbleRowClassName, + humanBubbleContentClassName, + botBubbleContentClassName, + codeBlocksCopyButtonClassName, + bubbleFooterClassName, + bubbleLoaderDotClassName, +}) => { + const classNamePrefix = message.speaker === 'human' ? 'human' : 'bot' + return ( +
+
+
+ {message.displayText ? ( + <> + + {message.contextFiles && message.contextFiles.length > 0 && ( + + )} + + ) : inProgress ? ( + + ) : null} +
+
+ {inProgress ? ( + Cody is typing... + ) : ( +
{`${ + message.speaker === 'assistant' ? 'Cody' : 'Me' + } · ${message.timestamp}`}
+ )} +
+
+
+ ) +} diff --git a/client/cody-ui/src/chat/ChatMessages.story.tsx b/client/cody-ui/src/chat/ChatMessages.story.tsx new file mode 100644 index 00000000000..8b237eec334 --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessages.story.tsx @@ -0,0 +1,45 @@ +import { Meta, Story } from '@storybook/react' + +import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' + +import { ChatMessages } from './ChatMessages' +import { FileLinkProps } from './ContextFiles' + +import styles from '../../../cody-web/src/Chat.module.css' + +const config: Meta = { + title: 'cody-ui/ChatMessages', + component: ChatMessages, + + decorators: [story =>
{story()}
], + + parameters: { + component: ChatMessages, + chromatic: { + enableDarkMode: true, + disableSnapshot: false, + }, + }, +} + +export default config + +const FIXTURE_TRANSCRIPT: ChatMessage[] = [ + { speaker: 'human', text: 'Hello, world!', displayText: 'Hello, world!', timestamp: '2 min ago' }, + { speaker: 'assistant', text: 'Thank you', displayText: 'Thank you', timestamp: 'now' }, +] + +export const Simple: Story = () => ( + +) + +const FileLink: React.FunctionComponent = ({ path }) => <>{path} diff --git a/client/cody-ui/src/chat/ChatMessages.tsx b/client/cody-ui/src/chat/ChatMessages.tsx new file mode 100644 index 00000000000..8d5c380e8a6 --- /dev/null +++ b/client/cody-ui/src/chat/ChatMessages.tsx @@ -0,0 +1,65 @@ +import React from 'react' + +import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' + +import { ChatMessageRow, ChatMessageRowClassNames } from './ChatMessageRow' +import { FileLinkProps } from './ContextFiles' + +export interface ChatMessagesClassNames extends ChatMessageRowClassNames {} + +export const ChatMessages: React.FunctionComponent< + { + messageInProgress: ChatMessage | null + transcript: ChatMessage[] + fileLinkComponent: React.FunctionComponent + className?: string + } & ChatMessagesClassNames +> = ({ + messageInProgress, + transcript, + fileLinkComponent, + className, + bubbleContentClassName, + bubbleClassName, + bubbleRowClassName, + humanBubbleContentClassName, + botBubbleContentClassName, + codeBlocksCopyButtonClassName, + bubbleFooterClassName, + bubbleLoaderDotClassName, +}) => ( +
+ {transcript.map((message, index) => ( + + ))} + {messageInProgress && messageInProgress.speaker === 'assistant' && ( + + )} +
+) diff --git a/client/cody/webviews/index.css b/client/cody/webviews/index.css index e43cd91f781..9157bb0a980 100644 --- a/client/cody/webviews/index.css +++ b/client/cody/webviews/index.css @@ -21,9 +21,6 @@ body[data-vscode-theme-kind='vscode-light'], body[data-vscode-theme-kind='vscode-high-contrast-light'] { --human-bubble-color: var(--vscode-button-background); --human-text-color: var(--vscode-button-foreground); - --bot-bubble-color: var(--vscode-input-background); - --bubble-text-color: var(--vscode-input-foreground); - --code-background: var(--vscode-editor-background); } html, diff --git a/client/storybook/src/main.ts b/client/storybook/src/main.ts index 10d1a7d5740..3b563fa2f0a 100644 --- a/client/storybook/src/main.ts +++ b/client/storybook/src/main.ts @@ -43,7 +43,7 @@ const getStoriesGlob = (): string[] => { // Due to an issue with constant recompiling (https://github.com/storybookjs/storybook/issues/14342) // we need to make the globs more specific (`(web|shared..)` also doesn't work). Once the above issue // is fixed, this can be removed and watched for `client/**/*.story.tsx` again. - const directoriesWithStories = ['branded', 'browser', 'jetbrains/webview', 'shared', 'web', 'wildcard'] + const directoriesWithStories = ['branded', 'browser', 'jetbrains/webview', 'shared', 'web', 'wildcard', 'cody-ui'] const storiesGlobs = directoriesWithStories.map(packageDirectory => path.resolve(ROOT_PATH, `client/${packageDirectory}/src/**/*.story.tsx`) ) @@ -184,8 +184,8 @@ const config: Config = { }) config.module?.rules.unshift({ - test: /\.(sass|scss)$/, - include: /\.module\.(sass|scss)$/, + test: /\.(sass|scss|css)$/, + include: /\.module\.(sass|scss|css)$/, exclude: storybookPath, use: getCSSLoaders( 'style-loader', @@ -206,7 +206,7 @@ const config: Config = { // CSS rule for external plain CSS (skip SASS and PostCSS for build perf) test: /\.css$/, // Make sure Storybook styles get handled by the Storybook config - exclude: [storybookPath, monacoEditorPath], + exclude: [storybookPath, monacoEditorPath, /\.module\.css$/], use: ['@terminus-term/to-string-loader', getBasicCSSLoader()], })