Skip to content

Commit

Permalink
feat: enhance AI assistant ui/x (#583)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimniels authored Jul 7, 2023
1 parent 231a489 commit ad68415
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 393 deletions.
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"ace-builds": "^1.4.13",
"axios": "^0.24.0",
"color": "^4.2.3",
"common-tags": "^1.8.2",
"date-fns": "^2.28.0",
"fontfaceobserver": "^2.3.0",
"fuzzysort": "^2.0.4",
Expand Down Expand Up @@ -108,6 +109,7 @@
},
"devDependencies": {
"@playwright/test": "^1.22.2",
"@types/common-tags": "^1.8.1",
"@types/lodash.debounce": "^4.0.7",
"@types/mixpanel-browser": "^2.38.1",
"@types/papaparse": "^5.3.7",
Expand Down
92 changes: 92 additions & 0 deletions src/ui/components/CodeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useRef, useState } from 'react';
import Editor from '@monaco-editor/react';
import { Box, IconButton, Stack, useTheme } from '@mui/material';
import { ContentCopy } from '@mui/icons-material';
import { TooltipHint } from './TooltipHint';
import { codeEditorBaseStyles } from '../menus/CodeEditor/styles';

interface Props {
code: string;
language?: string;
}

export function CodeSnippet({ code, language = 'plaintext' }: Props) {
const [tooltipMsg, setTooltipMsg] = useState<string>('Click to copy');
const editorRef = useRef(null);
const theme = useTheme();

const handleClick = (e: any) => {
if (editorRef.current) {
navigator.clipboard.writeText(code);
setTooltipMsg('Copied!');
setTimeout(() => {
setTooltipMsg('Click to copy');
}, 2000);
}
};

const handleEditorDidMount = (editor: any) => {
editorRef.current = editor;
};

return (
<Box style={codeEditorBaseStyles}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={1}
sx={{
backgroundColor: theme.palette.grey['100'],
pt: theme.spacing(0.5),
pb: theme.spacing(0.5),
// 10px on Monaco + 2px border
pr: '12px',
pl: '12px',
}}
>
<Box sx={{ color: 'text.secondary' }}>{language}</Box>

<TooltipHint title={tooltipMsg}>
<IconButton onClick={handleClick} size="small">
<ContentCopy fontSize="inherit" />
</IconButton>
</TooltipHint>
</Stack>
<div
style={{
// calculate height based on number of lines
height: `${Math.ceil(code.split('\n').length) * 19}px`,
position: 'relative',
border: `2px solid ${theme.palette.grey['100']}`,
borderTop: 'none',
}}
>
<Editor
language={language}
value={code}
height="100%"
width="100%"
options={{
readOnly: true,
minimap: { enabled: false },
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
scrollbar: {
vertical: 'hidden',
handleMouseWheel: false,
},
scrollBeyondLastLine: false,
wordWrap: 'off',
lineNumbers: 'off',
automaticLayout: true,
folding: false,
renderLineHighlightOnlyWhenFocus: true,
}}
onMount={handleEditorDidMount}
/>
</div>
</Box>
);
}
47 changes: 47 additions & 0 deletions src/ui/menus/CodeEditor/AICodeBlockParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { parseCodeBlocks } from './AICodeBlockParser';

describe('parseCodeBlocks()', () => {
test('A string without code blocks returns one item', () => {
const result = parseCodeBlocks(`This is a string from AI.`);
expect(result).toHaveLength(1);
expect(typeof result[0]).toBe('string');
});

test('A string + 1 unclosed code block returns two items', () => {
const result = parseCodeBlocks(`
A string of text with an open code block.
\`\`\`js
const foo = 'foo';`);
expect(result).toHaveLength(2);
expect(typeof result[0]).toBe('string');
expect(typeof result[1]).toBe('object');
});

test('A string + 1 code block returns two items', () => {
const result = parseCodeBlocks(`
A string of text with a full code block.
\`\`\`js
const foo = 'foo';
\`\`\``);
expect(result).toHaveLength(2);
});

test('A string + 1 full code block + a string returns three items', () => {
const result = parseCodeBlocks(`
A string of text with a full code block.
\`\`\`js
const foo = 'foo';
\`\`\`
And another piece of text
And more text here`);
expect(result).toHaveLength(3);
expect(typeof result[0]).toBe('string');
expect(typeof result[1]).toBe('object');
expect(typeof result[2]).toBe('string');
});
});
47 changes: 10 additions & 37 deletions src/ui/menus/CodeEditor/AICodeBlockParser.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Editor from '@monaco-editor/react';
import { Stack } from '@mui/material';
import { CodeSnippet } from '../../components/CodeSnippet';

const CODE_BLOCK_REGEX = /```([a-z]+)?\n([\s\S]+?)\n```/g;
const CODE_BLOCK_REGEX = /```([a-z]+)?\n([\s\S]+?)(?:\n```|$)/g;

function parseCodeBlocks(input: string): Array<string | JSX.Element> {
export function parseCodeBlocks(input: string): Array<string | JSX.Element> {
const blocks: Array<string | JSX.Element> = [];
let match;
let lastIndex = 0;
Expand All @@ -12,39 +13,7 @@ function parseCodeBlocks(input: string): Array<string | JSX.Element> {
if (lastIndex < match.index) {
blocks.push(input.substring(lastIndex, match.index));
}
blocks.push(
<div
key={lastIndex}
// calculate height based on number of lines
style={{
height: `${Math.ceil(code.split('\n').length) * 19}px`,
width: '100%',
}}
>
<Editor
language={language}
value={code}
height="100%"
width="100%"
options={{
readOnly: true,
minimap: { enabled: false },
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
handleMouseWheel: false,
},
scrollBeyondLastLine: false,
wordWrap: 'off',
// lineNumbers: 'off',
automaticLayout: true,
}}
/>
</div>
);
blocks.push(<CodeSnippet key={lastIndex} code={code} language={language} />);
lastIndex = match.index + match[0].length;
}
if (lastIndex < input.length) {
Expand All @@ -54,5 +23,9 @@ function parseCodeBlocks(input: string): Array<string | JSX.Element> {
}

export function CodeBlockParser({ input }: { input: string }): JSX.Element {
return <>{parseCodeBlocks(input)}</>;
return (
<Stack gap={2} style={{ whiteSpace: 'normal' }}>
{parseCodeBlocks(input)}
</Stack>
);
}
Loading

1 comment on commit ad68415

@vercel
Copy link

@vercel vercel bot commented on ad68415 Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

quadratic – ./

quadratic-nu.vercel.app
quadratic-quadratic.vercel.app
quadratic-git-main-quadratic.vercel.app

Please sign in to comment.