mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
show cody context files as an action in the transcript (#50691)
Instead of showing these in a longer message after Cody's response, the files that Cody read are now shown in a small chip before the response: `7 files read`. Expanding it shows the first step (searching) and each file. This UI can be extended to more kinds of plugins/actions in the future.   ## Test plan Open Cody. Run a query that requires it to read files. Confirm they show up here.
This commit is contained in:
parent
52e0c4e4a4
commit
72f95daf58
1
client/cody-ui/BUILD.bazel
generated
1
client/cody-ui/BUILD.bazel
generated
@ -32,6 +32,7 @@ ts_project(
|
||||
"src/chat/Transcript.story.tsx",
|
||||
"src/chat/Transcript.tsx",
|
||||
"src/chat/TranscriptItem.tsx",
|
||||
"src/chat/actions/TranscriptAction.tsx",
|
||||
"src/chat/fixtures.ts",
|
||||
"src/chat/inputContext/ChatInputContext.story.tsx",
|
||||
"src/chat/inputContext/ChatInputContext.tsx",
|
||||
|
||||
@ -72,6 +72,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
transcriptItemClassName,
|
||||
humanTranscriptItemClassName,
|
||||
transcriptItemParticipantClassName,
|
||||
transcriptActionClassName,
|
||||
inputRowClassName,
|
||||
chatInputContextClassName,
|
||||
chatInputClassName,
|
||||
@ -148,6 +149,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
transcriptItemClassName={transcriptItemClassName}
|
||||
humanTranscriptItemClassName={humanTranscriptItemClassName}
|
||||
transcriptItemParticipantClassName={transcriptItemParticipantClassName}
|
||||
transcriptActionClassName={transcriptActionClassName}
|
||||
className={styles.transcriptContainer}
|
||||
/>
|
||||
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
.context-files-collapsed {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-files-expanded {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.context-files-toggle-icon {
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.context-files-list-title {
|
||||
cursor: pointer;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.context-files-list-container {
|
||||
margin-top: 0;
|
||||
padding-inline-start: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.context-files-list-container li {
|
||||
padding: 0.1rem;
|
||||
/* stylelint-disable-next-line declaration-property-unit-allowed-list */
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.context-files-collapsed .context-files-toggle-icon {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.context-files-collapsed-text {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-style: italic;
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { mdiChevronDown, mdiChevronRight } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { mdiFileDocumentOutline, mdiMagnify } from '@mdi/js'
|
||||
|
||||
import styles from './ContextFiles.module.css'
|
||||
import { pluralize } from '@sourcegraph/common'
|
||||
|
||||
import { TranscriptAction } from './actions/TranscriptAction'
|
||||
|
||||
export interface FileLinkProps {
|
||||
path: string
|
||||
@ -15,60 +14,17 @@ export const ContextFiles: React.FunctionComponent<{
|
||||
contextFiles: string[]
|
||||
fileLinkComponent: React.FunctionComponent<FileLinkProps>
|
||||
className?: string
|
||||
}> = ({ contextFiles, fileLinkComponent: FileLink, className }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
if (contextFiles.length === 1) {
|
||||
return (
|
||||
<p className={className}>
|
||||
Cody read{' '}
|
||||
<span className={styles.contextFile}>
|
||||
<FileLink path={contextFiles[0]} />
|
||||
</span>{' '}
|
||||
to provide an answer.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
return (
|
||||
<div className={classNames(className, styles.contextFilesExpanded)}>
|
||||
<span className={styles.contextFilesToggleIcon} onClick={() => setIsExpanded(false)}>
|
||||
<Icon aria-hidden={true} svgPath={mdiChevronDown} />
|
||||
</span>
|
||||
<div>
|
||||
<div className={styles.contextFilesListTitle} onClick={() => setIsExpanded(false)}>
|
||||
Cody read the following files to provide an answer:
|
||||
</div>
|
||||
<ul className={styles.contextFilesListContainer}>
|
||||
{contextFiles.map(file => (
|
||||
<li key={file} className={styles.contextFile}>
|
||||
<FileLink path={file} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.contextFilesCollapsed)} onClick={() => setIsExpanded(true)}>
|
||||
<span className={styles.contextFilesToggleIcon}>
|
||||
<Icon aria-hidden={true} svgPath={mdiChevronRight} />
|
||||
</span>
|
||||
<div className={styles.contextFilesCollapsedText}>
|
||||
<span>
|
||||
Cody read <span className={styles.contextFile}>{contextFiles[0].split('/').pop()}</span> and{' '}
|
||||
{contextFiles.length - 1} other {contextFiles.length > 2 ? 'files' : 'file'} to provide an answer.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Icon: React.FC<{ svgPath: string }> = ({ svgPath }) => (
|
||||
<svg role="img" height={24} width={24} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d={svgPath} />
|
||||
</svg>
|
||||
}> = ({ contextFiles, fileLinkComponent: FileLink, className }) => (
|
||||
<TranscriptAction
|
||||
title={{ verb: 'Read', object: `${contextFiles.length} ${pluralize('file', contextFiles.length)}` }}
|
||||
steps={[
|
||||
{ verb: 'Searched', object: 'entire codebase for relevant files', icon: mdiMagnify },
|
||||
...contextFiles.map(file => ({
|
||||
verb: 'Read',
|
||||
object: <FileLink path={file} />,
|
||||
icon: mdiFileDocumentOutline,
|
||||
})),
|
||||
]}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
--row-foreground-color: black;
|
||||
--row-participant-name-color: #777;
|
||||
--row-border-color: #cccccc;
|
||||
--transcript-action-background-color: #00000011;
|
||||
}
|
||||
|
||||
:global(.theme-dark) {
|
||||
@ -12,6 +13,7 @@
|
||||
--row-foreground-color: #ffffffee;
|
||||
--row-participant-name-color: #ffffff88;
|
||||
--row-border-color: #ffffff33;
|
||||
--transcript-action-background-color: #ffffff11;
|
||||
}
|
||||
|
||||
.transcript-item {
|
||||
@ -35,3 +37,7 @@
|
||||
.transcript-item-participant {
|
||||
color: var(--row-participant-name-color);
|
||||
}
|
||||
|
||||
.transcript-action {
|
||||
background-color: var(--transcript-action-background-color);
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ export const Simple: ComponentStoryObj<typeof Transcript> = {
|
||||
transcriptItemClassName={styles.transcriptItem}
|
||||
humanTranscriptItemClassName={styles.humanTranscriptItem}
|
||||
transcriptItemParticipantClassName={styles.transcriptItemParticipant}
|
||||
transcriptActionClassName={styles.transcriptAction}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export const Transcript: React.FunctionComponent<
|
||||
transcriptItemClassName,
|
||||
humanTranscriptItemClassName,
|
||||
transcriptItemParticipantClassName,
|
||||
transcriptActionClassName,
|
||||
}) => {
|
||||
const transcriptContainerRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
@ -71,6 +72,7 @@ export const Transcript: React.FunctionComponent<
|
||||
transcriptItemClassName={transcriptItemClassName}
|
||||
humanTranscriptItemClassName={humanTranscriptItemClassName}
|
||||
transcriptItemParticipantClassName={transcriptItemParticipantClassName}
|
||||
transcriptActionClassName={transcriptActionClassName}
|
||||
/>
|
||||
))}
|
||||
{messageInProgress && messageInProgress.speaker === 'assistant' && (
|
||||
@ -81,6 +83,7 @@ export const Transcript: React.FunctionComponent<
|
||||
codeBlocksCopyButtonClassName={codeBlocksCopyButtonClassName}
|
||||
transcriptItemClassName={transcriptItemClassName}
|
||||
transcriptItemParticipantClassName={transcriptItemParticipantClassName}
|
||||
transcriptActionClassName={transcriptActionClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -46,6 +46,14 @@
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--spacing) * 0.5);
|
||||
padding: 0 var(--spacing);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--spacing) var(--spacing) var(--spacing);
|
||||
word-break: break-word;
|
||||
@ -63,8 +71,3 @@
|
||||
.content > div:first-child > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.context-files {
|
||||
margin-top: var(--spacing);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export interface TranscriptItemClassNames {
|
||||
humanTranscriptItemClassName?: string
|
||||
transcriptItemParticipantClassName?: string
|
||||
codeBlocksCopyButtonClassName?: string
|
||||
transcriptActionClassName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,6 +40,7 @@ export const TranscriptItem: React.FunctionComponent<
|
||||
humanTranscriptItemClassName,
|
||||
transcriptItemParticipantClassName,
|
||||
codeBlocksCopyButtonClassName,
|
||||
transcriptActionClassName,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
@ -58,18 +60,18 @@ export const TranscriptItem: React.FunctionComponent<
|
||||
)}
|
||||
</h2>
|
||||
</header>
|
||||
{message.contextFiles && message.contextFiles.length > 0 && (
|
||||
<div className={styles.actions}>
|
||||
<ContextFiles
|
||||
contextFiles={message.contextFiles}
|
||||
fileLinkComponent={fileLinkComponent}
|
||||
className={transcriptActionClassName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.content)}>
|
||||
{message.displayText ? (
|
||||
<>
|
||||
<CodeBlocks displayText={message.displayText} copyButtonClassName={codeBlocksCopyButtonClassName} />
|
||||
{message.contextFiles && message.contextFiles.length > 0 && (
|
||||
<ContextFiles
|
||||
contextFiles={message.contextFiles}
|
||||
fileLinkComponent={fileLinkComponent}
|
||||
className={styles.contextFiles}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<CodeBlocks displayText={message.displayText} copyButtonClassName={codeBlocksCopyButtonClassName} />
|
||||
) : inProgress ? (
|
||||
<BlinkingCursor />
|
||||
) : null}
|
||||
|
||||
60
client/cody-ui/src/chat/actions/TranscriptAction.module.css
Normal file
60
client/cody-ui/src/chat/actions/TranscriptAction.module.css
Normal file
@ -0,0 +1,60 @@
|
||||
.container {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.4rem 0.3rem 0.7rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.container-open {
|
||||
padding-bottom: 0.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.open-close-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: currentColor;
|
||||
font-family: inherit;
|
||||
}
|
||||
.open-close-icon {
|
||||
opacity: 0.8;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.step-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.step-object {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
62
client/cody-ui/src/chat/actions/TranscriptAction.tsx
Normal file
62
client/cody-ui/src/chat/actions/TranscriptAction.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import styles from './TranscriptAction.module.css'
|
||||
|
||||
export interface TranscriptActionStep {
|
||||
verb: string
|
||||
object: string | JSX.Element
|
||||
|
||||
/**
|
||||
* The SVG path of an icon.
|
||||
*
|
||||
* @example mdiSearchWeb
|
||||
*/
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export const TranscriptAction: React.FunctionComponent<{
|
||||
title: string | { verb: string; object: string }
|
||||
steps: TranscriptActionStep[]
|
||||
className?: string
|
||||
}> = ({ title, steps, className }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.container, open && styles.containerOpen)}>
|
||||
<button type="button" onClick={() => setOpen(!open)} className={styles.openCloseButton}>
|
||||
{typeof title === 'string' ? (
|
||||
title
|
||||
) : (
|
||||
<span>
|
||||
{title.verb} <strong>{title.object}</strong>
|
||||
</span>
|
||||
)}
|
||||
<Icon
|
||||
aria-hidden={true}
|
||||
svgPath={open ? mdiChevronUp : mdiChevronDown}
|
||||
className={styles.openCloseIcon}
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<ol className={styles.steps}>
|
||||
{steps.map((step, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<li key={index} className={styles.step}>
|
||||
{step.icon && <Icon svgPath={step.icon} className={styles.stepIcon} />} {step.verb}{' '}
|
||||
<span className={styles.stepObject}>{step.object}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Icon: React.FunctionComponent<{ svgPath: string; className?: string }> = ({ svgPath, className }) => (
|
||||
<svg role="img" height={24} width={24} viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d={svgPath} />
|
||||
</svg>
|
||||
)
|
||||
@ -12,10 +12,10 @@
|
||||
"exclude": ["out", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../common",
|
||||
"path": "../cody-shared",
|
||||
},
|
||||
{
|
||||
"path": "../cody-shared",
|
||||
"path": "../common",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@ -35,6 +35,10 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.transcript-action {
|
||||
background-color: Field;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
background-color: Canvas;
|
||||
border-top: solid 1px ButtonBorder;
|
||||
|
||||
@ -38,6 +38,7 @@ export const Chat: React.FunctionComponent<
|
||||
transcriptItemClassName={styles.transcriptItem}
|
||||
humanTranscriptItemClassName={styles.humanTranscriptItem}
|
||||
transcriptItemParticipantClassName={styles.transcriptItemParticipant}
|
||||
transcriptActionClassName={styles.transcriptAction}
|
||||
inputRowClassName={styles.inputRow}
|
||||
chatInputClassName={styles.chatInput}
|
||||
/>
|
||||
|
||||
@ -38,6 +38,10 @@ body[data-vscode-theme-kind='vscode-high-contrast'] .human-transcript-item {
|
||||
color: var(--vscode-input-foreground);
|
||||
}
|
||||
|
||||
.transcript-action {
|
||||
background-color: var(--code-background);
|
||||
}
|
||||
|
||||
.code-blocks-copy-button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
|
||||
@ -54,6 +54,7 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
|
||||
transcriptItemClassName={styles.transcriptItem}
|
||||
humanTranscriptItemClassName={styles.humanTranscriptItem}
|
||||
transcriptItemParticipantClassName={styles.transcriptItemParticipant}
|
||||
transcriptActionClassName={styles.transcriptAction}
|
||||
inputRowClassName={styles.inputRow}
|
||||
chatInputContextClassName={styles.chatInputContext}
|
||||
chatInputClassName={styles.chatInputClassName}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
display: inline;
|
||||
display: contents; /* so our parent can tell us to truncate with ellipsis */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
|
||||
1210
pnpm-lock.yaml
1210
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user