Skip to content

Commit 0030afe

Browse files
authored
add copyable code blocks to Copilot Search response (#56126)
1 parent aed0f49 commit 0030afe

File tree

3 files changed

+68
-6
lines changed

3 files changed

+68
-6
lines changed

data/ui.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ search:
6161
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
6262
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
6363
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
64+
response:
65+
copy_code: Copy code to clipboard
66+
copied_code: Copied!
6467
failure:
6568
general_title: There was an error loading search results.
6669
ai_title: There was an error loading Copilot.

src/fixtures/fixtures/data/ui.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ search:
6161
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
6262
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
6363
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
64+
response:
65+
copy_code: Copy code to clipboard
66+
copied_code: Copied!
6467
failure:
6568
general_title: There was an error loading search results.
6669
ai_title: There was an error loading Copilot.

src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,91 @@
1+
import React from 'react'
12
import ReactMarkdown from 'react-markdown'
23
import type { Components } from 'react-markdown'
3-
import cx from 'classnames'
44
import remarkGfm from 'remark-gfm'
5+
import cx from 'classnames'
6+
import { IconButton } from '@primer/react'
7+
import { CopyIcon, CheckIcon } from '@primer/octicons-react'
8+
import { announce } from '@primer/live-region-element'
9+
10+
import { useTranslation } from '@/languages/components/useTranslation'
11+
import useCopyClipboard from '@/rest/components/useClipboard'
12+
import { EventType } from '@/events/types'
13+
import { sendEvent } from '@/events/components/events'
514

615
export type MarkdownContentPropsT = {
716
children: string
817
className?: string
918
openLinksInNewTab?: boolean
1019
includeQueryParams?: boolean
20+
codeBlocksCopyable?: boolean
1121
eventGroupKey?: string
1222
eventGroupId?: string
1323
as?: keyof JSX.IntrinsicElements
1424
tabIndex?: number
1525
}
1626

17-
// For content that comes in a Markdown string
18-
// e.g. a GPT Response
19-
2027
export const UnrenderedMarkdownContent = ({
2128
children,
2229
className,
2330
openLinksInNewTab = true,
2431
includeQueryParams = true,
32+
codeBlocksCopyable = true,
2533
eventGroupKey = '',
2634
eventGroupId = '',
2735
...restProps
2836
}: MarkdownContentPropsT) => {
37+
const { t } = useTranslation('search')
2938
// Overrides for ReactMarkdown components
3039
const components = {} as Components
31-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
32-
components.a = ({ node, ...props }) => {
40+
if (codeBlocksCopyable) {
41+
// Override the default code block to make multiline code blocks copyable
42+
components.code = ({ ...props }) => {
43+
// get the literal text of the code block
44+
let text = String(props.children)
45+
// If the codeblock is not multiline, return inline code block without copy functionality
46+
if (!text.includes('\n')) {
47+
return <code {...props}>{props.children}</code>
48+
} else {
49+
// Otherwise it's multiline and we want to make it copyable
50+
text = text.replace(/\n$/, '')
51+
}
52+
53+
const [isCopied, copyToClipboard] = useCopyClipboard(text, {
54+
successDuration: 2000,
55+
})
56+
57+
return (
58+
<div>
59+
<IconButton
60+
size="small"
61+
icon={isCopied ? CheckIcon : CopyIcon}
62+
className="btn-octicon"
63+
aria-label={t('search.ai.response.copy_code')}
64+
onClick={async () => {
65+
await copyToClipboard()
66+
announce(t('search.ai.response.copied_code'))
67+
sendEvent({
68+
type: EventType.clipboard,
69+
clipboard_operation: 'copy',
70+
eventGroupKey: eventGroupKey,
71+
eventGroupId: eventGroupId,
72+
})
73+
}}
74+
sx={{
75+
position: 'absolute',
76+
right: '1.3rem',
77+
marginTop: '-.7rem',
78+
zIndex: 1,
79+
}}
80+
></IconButton>
81+
<code {...props}>{props.children}</code>
82+
</div>
83+
)
84+
}
85+
}
86+
87+
// Override the default anchor tag to open links in a new tab and include specific query parameters
88+
components.a = ({ ...props }) => {
3389
let href = props.href || ''
3490
let existingAnchorParams = ''
3591
// When we want to include specific query parameters in the URL

0 commit comments

Comments
 (0)