Skip to content

Commit

Permalink
Add copy button to code dialog (#5336)
Browse files Browse the repository at this point in the history
* Add copy button to code dialog

* Add entry

* Move copy button into code block

* Update

* Fix max width

* Fix screenshots
  • Loading branch information
compulim authored Oct 28, 2024
1 parent f1d7b19 commit 4a18bde
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 131 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Added MathML/TeX block support in Markdown via [`micromark-extension-math`](https://npmjs.com/package/micromark-extension-math) and [`katex`](https://katex.org/), in PR [#5332](https://github.com/microsoft/BotFramework-WebChat/pull/5332), by [@compulim](https://github.com/compulim)
- Added code viewer dialog with syntax highlighting, in PR [#5335](https://github.com/microsoft/BotFramework-WebChat/pull/5335), by [@OEvgeny](https://github.com/OEvgeny)
- Added copy button to code blocks, in PR [#5334](https://github.com/microsoft/BotFramework-WebChat/pull/5334), by [@compulim](https://github.com/compulim)
- Added copy button to view code dialog, in PR [#5336](https://github.com/microsoft/BotFramework-WebChat/pull/5336), by [@compulim](https://github.com/compulim)

### Changed

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions __tests__/html2/activity/behavior.copyCode.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat" style="position: relative"></main>
<script type="text/babel">
const CODE = `import numpy as np
import matplotlib.pyplot as plt
def plot_sine_waves():
"""Create a beautiful visualization of sine waves with different frequencies."""
# Generate time points
t = np.linspace(0, 10, 1000)
# Create waves with different frequencies and phases
wave1 = np.sin(t)
wave2 = 0.5 * np.sin(2 * t + np.pi/4)
wave3 = 0.3 * np.sin(3 * t + np.pi/3)
# Combine waves
combined = wave1 + wave2 + wave3
# Create a stylish plot
plt.style.use('seaborn-darkgrid')
plt.figure(figsize=(12, 8))
# Plot individual waves
plt.plot(t, wave1, label='Primary Wave', alpha=0.5)
plt.plot(t, wave2, label='Second Harmonic', alpha=0.5)
plt.plot(t, wave3, label='Third Harmonic', alpha=0.5)
# Plot combined wave with a thicker line
plt.plot(t, combined, 'r-',
label='Combined Wave',
linewidth=2)
plt.title('Harmonic Wave Composition', fontsize=14)
plt.xlabel('Time', fontsize=12)
plt.ylabel('Amplitude', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
# Show the plot
plt.tight_layout()
plt.show()
# Generate the visualization
plot_sine_waves()`;

run(async function () {
const {
React,
ReactDOM: { render },
WebChat: { ReactWebChat, testIds }
} = window; // Imports in UMD fashion.

const aiMessageEntity = {
'@context': 'https://schema.org',
'@id': '',
'@type': 'Message',
keywords: ['AIGeneratedContent', 'AllowCopy'],
type: 'https://schema.org/Message'
};

const waveSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`
<svg width="400" height="200" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Primary Wave -->
<path d="M0,100 C50,50 100,150 150,100 C200,50 250,150 300,100 C350,50 400,150 400,100"
stroke="#3B82F6" fill="none" stroke-width="2" opacity="0.5"/>
<!-- Second Harmonic -->
<path d="M0,100 C25,75 50,125 75,100 C100,75 125,125 150,100 C175,75 200,125 225,100 C250,75 275,125 300,100 C325,75 350,125 375,100 C400,75 400,125 400,100"
stroke="#10B981" fill="none" stroke-width="2" opacity="0.5"/>
<!-- Combined Wave -->
<path d="M0,100 C40,30 80,170 120,100 C160,30 200,170 240,100 C280,30 320,170 360,100 C380,65 400,135 400,100"
stroke="#EF4444" fill="none" stroke-width="3"/>
<!-- Grid Lines -->
<line x1="0" y1="100" x2="400" y2="100" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="100" y1="0" x2="100" y2="200" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="200" y1="0" x2="200" y2="200" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="300" y1="0" x2="300" y2="200" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
</svg>`)}`;

const { directLine, store } = testHelpers.createDirectLineEmulator();

const App = () => (
<React.Fragment>
<ReactWebChat directLine={directLine} store={store} />
<div style={{ gap: 8, position: 'absolute', top: 0, width: '100%' }}>
<label>
<div>Paste box</div>
<textarea
data-testid="paste box"
spellCheck={false}
style={{ background: '#f0f0f0', border: 0, height: 100, padding: 0, resize: 'none', width: '100%' }}
/>
</label>
</div>
</React.Fragment>
);

render(<App />, document.getElementById('webchat'));

await pageConditions.uiConnected();

directLine.emulateIncomingActivity({
from: { role: 'bot' },
entities: [
{
...aiMessageEntity,
isBasedOn: {
'@type': 'SoftwareSourceCode',
programmingLanguage: 'python',
text: CODE
}
}
],
type: 'message',
text: `This example demonstrates creating a beautiful visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns.\n<img alt="wave plot" src="${waveSvg}">`
});

await pageConditions.numActivitiesShown(1);

pageElements.sendBoxTextBox().focus();

await host.sendShiftTab(3);
await host.sendKeys('ENTER', 'ENTER');

await pageConditions.became('dialog to show up', () => document.activeElement.closest('dialog'), 1_000);

await host.sendTab();

expect(document.activeElement).toBe(document.querySelector(`[data-testid="${testIds.codeBlockCopyButton}"]`));

await host.sendKeys('ENTER', 'ESCAPE');

// WHEN: Paste into plain text text box.
await host.click(document.querySelector('[data-testid="paste box"]'));
await host.sendKeys('+CONTROL', 'v', '-CONTROL');

// THEN: Should past into the text box.
expect(document.querySelector('[data-testid="paste box"]').value).toBe(CODE);
});
</script>
</body>
</html>
33 changes: 16 additions & 17 deletions __tests__/html2/activity/viewCodeButton.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@
const waveSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`
<svg width="400" height="200" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Primary Wave -->
<path d="M0,100 C50,50 100,150 150,100 C200,50 250,150 300,100 C350,50 400,150 400,100"
<path d="M0,100 C50,50 100,150 150,100 C200,50 250,150 300,100 C350,50 400,150 400,100"
stroke="#3B82F6" fill="none" stroke-width="2" opacity="0.5"/>
<!-- Second Harmonic -->
<path d="M0,100 C25,75 50,125 75,100 C100,75 125,125 150,100 C175,75 200,125 225,100 C250,75 275,125 300,100 C325,75 350,125 375,100 C400,75 400,125 400,100"
<path d="M0,100 C25,75 50,125 75,100 C100,75 125,125 150,100 C175,75 200,125 225,100 C250,75 275,125 300,100 C325,75 350,125 375,100 C400,75 400,125 400,100"
stroke="#10B981" fill="none" stroke-width="2" opacity="0.5"/>
<!-- Combined Wave -->
<path d="M0,100 C40,30 80,170 120,100 C160,30 200,170 240,100 C280,30 320,170 360,100 C380,65 400,135 400,100"
<path d="M0,100 C40,30 80,170 120,100 C160,30 200,170 240,100 C280,30 320,170 360,100 C380,65 400,135 400,100"
stroke="#EF4444" fill="none" stroke-width="3"/>
<!-- Grid Lines -->
<line x1="0" y1="100" x2="400" y2="100" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="100" y1="0" x2="100" y2="200" stroke="#CBD5E1" stroke-width="0.5" stroke-dasharray="4"/>
Expand All @@ -56,7 +56,7 @@
);

await pageConditions.uiConnected();

directLine.emulateIncomingActivity({
from: { role: 'bot' },
entities: [{
Expand All @@ -71,35 +71,35 @@
"""Create a beautiful visualization of sine waves with different frequencies."""
# Generate time points
t = np.linspace(0, 10, 1000)
# Create waves with different frequencies and phases
wave1 = np.sin(t)
wave2 = 0.5 * np.sin(2 * t + np.pi/4)
wave3 = 0.3 * np.sin(3 * t + np.pi/3)
# Combine waves
combined = wave1 + wave2 + wave3
# Create a stylish plot
plt.style.use('seaborn-darkgrid')
plt.figure(figsize=(12, 8))
# Plot individual waves
plt.plot(t, wave1, label='Primary Wave', alpha=0.5)
plt.plot(t, wave2, label='Second Harmonic', alpha=0.5)
plt.plot(t, wave3, label='Third Harmonic', alpha=0.5)
# Plot combined wave with a thicker line
plt.plot(t, combined, 'r-',
label='Combined Wave',
plt.plot(t, combined, 'r-',
label='Combined Wave',
linewidth=2)
plt.title('Harmonic Wave Composition', fontsize=14)
plt.xlabel('Time', fontsize=12)
plt.ylabel('Amplitude', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
# Show the plot
plt.tight_layout()
plt.show()
Expand All @@ -112,7 +112,6 @@
text: `This example demonstrates creating a beautiful visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns.\n<img alt="wave plot" src="${waveSvg}">`,
});

console.log('HERE')
await pageConditions.numActivitiesShown(1);

// When: Focus the "Code" button
Expand Down
Binary file modified __tests__/html2/activity/viewCodeButton.html.snap-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion __tests__/html2/markdown/codeBlockCopyButton/behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat" style="position: relative"></main>
<main id="webchat" style="position: relative;"></main>
<script type="text/babel">
run(async function () {
await host.sendDevToolsCommand('Browser.setPermission', {
Expand Down
2 changes: 1 addition & 1 deletion packages/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"precommit:eslint": "../../node_modules/.bin/eslint --report-unused-disable-directives --max-warnings 0",
"precommit:typecheck": "tsc --project ./src --emitDeclarationOnly false --esModuleInterop true --noEmit --pretty false",
"preversion": "cat package.json | jq '(.localDependencies // {} | to_entries | map([if .value == \"production\" then \"dependencies\" else \"devDependencies\" end, .key])) as $P | delpaths($P)' > package-temp.json && mv package-temp.json package.json",
"start": "npm run build:tsup -- --onSuccess=\"touch ../bundle/src/bundle/index.ts\" --watch"
"start": "npm run build:tsup -- --onSuccess=\"touch ../bundle/src/FullComposer.tsx\" --watch"
},
"localDependencies": {
"botframework-webchat-api": "production",
Expand Down
31 changes: 24 additions & 7 deletions packages/component/src/Attachment/Text/private/CodeContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { Fragment, memo, ReactNode, useEffect, useState } from 'react';
import { hooks } from 'botframework-webchat-api';
import classNames from 'classnames';
import React, { Fragment, memo, ReactNode, useEffect, useState } from 'react';
import { useStyleSet } from '../../../hooks';
import CodeBlockCopyButton from '../../../providers/CustomElements/customElements/CodeBlockCopyButton';
import createHighlighter from './shiki';

type Props = Readonly<{
Expand All @@ -10,10 +13,17 @@ type Props = Readonly<{
title: string;
}>;

const { useLocalizer } = hooks;

const highlighterPromise = createHighlighter();

const CodeContent = memo(({ children, className, code, language, title }: Props) => {
const [highlightedCode, setHighlightedCode] = useState('');
const localize = useLocalizer();

const copiedAlt = localize('COPY_BUTTON_COPIED_TEXT');
const copyAlt = localize('COPY_BUTTON_TEXT');
const [{ codeBlockCopyButton: codeBlockCopyButtonClassName }] = useStyleSet();

useEffect(() => {
let mounted = true;
Expand Down Expand Up @@ -48,13 +58,20 @@ const CodeContent = memo(({ children, className, code, language, title }: Props)
<Fragment>
<div className={'webchat__view-code-dialog__header'}>
<h2 className={'webchat__view-code-dialog__title'}>{title}</h2>
{/* <CopyCodeButton htmlText={highlightedCode} plainText={code} /> */}
</div>
<div
className={classNames('webchat__view-code-dialog__body', className)}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
<div className={classNames('webchat__view-code-dialog__body')}>
<CodeBlockCopyButton
className={classNames('webchat__view-code-dialog__copy-button', codeBlockCopyButtonClassName)}
data-alt-copied={copiedAlt}
data-alt-copy={copyAlt}
data-value={code}
/>
<div
className={classNames('webchat__view-code-dialog__code-body', className)}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
</div>
{children}
</Fragment>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/component/src/Styles/StyleSet/ModalDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function createModalDialogStyleSet() {

'@media screen and (min-width: 640px)': {
maxWidth: '60%',
minWidth: 'calc(640px - 32px)',
minWidth: 'calc(640px - 2em - 6px)', // Browser natively do "100% - 2em - 6px".
width: '60%'
},

Expand Down
14 changes: 13 additions & 1 deletion packages/component/src/Styles/StyleSet/ViewCodeDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export default function createViewCodeDialogStyle() {
boxSizing: 'border-box',
display: 'grid',
height: '100%',
margin: 0,
maxHeight: '100vh',
overflow: 'hidden',
padding: '1rem',
Expand All @@ -33,6 +32,7 @@ export default function createViewCodeDialogStyle() {
},

'& .webchat__view-code-dialog__header': {
alignItems: 'center',
display: 'flex',
paddingInlineEnd: '30px'
},
Expand All @@ -43,13 +43,25 @@ export default function createViewCodeDialogStyle() {
},

'& .webchat__view-code-dialog__body': {
display: 'flex',
overflow: 'hidden',
position: 'relative'
},

'& .webchat__view-code-dialog__code-body': {
lineHeight: '20px',
overflow: 'auto'
},

'& .webchat__view-code-dialog__footer': {
color: CSSTokens.ColorSubtle,
lineHeight: '20px'
},

'& .webchat__view-code-dialog__copy-button': {
position: 'absolute',
right: '10px',
top: '10px'
}
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import mathRandom from 'math-random';
import React, { memo, useCallback, useMemo, type ReactNode } from 'react';
import CodeBlockCopyButtonElement from './customElements/CodeBlockCopyButtonWithReact';
import { CodeBlockCopyButtonElement } from './customElements/CodeBlockCopyButton';
import CustomElementsContext from './private/CustomElementsContext';

type CustomElementsComposerProps = Readonly<{
Expand Down
Loading

0 comments on commit 4a18bde

Please sign in to comment.