Skip to content

Commit

Permalink
feat: AI ui/ux enhancements (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimniels committed May 5, 2023
1 parent ca44cb8 commit 56bb6a5
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 75 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
![Twitter Follow](https://img.shields.io/twitter/follow/QuadraticHQ)
![quadraticlogo4 1](https://user-images.githubusercontent.com/3479421/162037216-2fea1620-2310-4cfa-96fb-31299195e3a9.png)

## ![quardatic icon small](https://user-images.githubusercontent.com/3479421/162039117-02f85f2c-e382-4ed8-ac39-64efab17a144.svg) **_The Data Science Spreadsheet_**
## ![quardatic icon small](https://user-images.githubusercontent.com/3479421/162039117-02f85f2c-e382-4ed8-ac39-64efab17a144.svg) **_The data science spreadsheet_**

Infinite data grid with Python, JavaScript, and SQL built-in. Data Connectors to pull in your data.

Take your data and do something useful with it as quickly and easily as possible!

<img width="1552" alt="Screenshot 2023-02-24 at 2 57 36 PM" src="https://user-images.githubusercontent.com/3479421/221301059-921ad96a-878e-4082-b3b9-e55a54185c5d.png">

## Online Demo
## Online demo

We have a hosted version of the `main` branch available online.

Expand Down Expand Up @@ -39,9 +39,9 @@ Quadratic has no environment to configure. The grid runs entirely in the browser
- Quickly mix data from different sources
- Explore your data for new insights

# Getting Started
# Getting started

### Run Quadratic Locally
### Run Quadratic locally

1. Install npm, [rustup](https://www.rust-lang.org/tools/install), and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/)
2. Run `rustup target add wasm32-unknown-unknown`
Expand All @@ -52,7 +52,7 @@ Run Web `npm start`

Run Electron `npm run dev`

# Development Progress and Roadmap
# Development progress and roadmap

_Quadratic is in ALPHA. For now, we do not recommend relying on Quadratic._

Expand All @@ -79,6 +79,6 @@ There are more example files are in the application file menu. File > Open sampl

You can download them and then open them in Quadratic via File > Open Grid

## Quadratic is Hiring
## Quadratic is hiring

Check out our open roles -> [careers.quadratichq.com](https://careers.quadratichq.com)
13 changes: 13 additions & 0 deletions src/ui/components/ConditionalWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

interface ConditionalWrapperProps {
children: React.ReactNode;
condition: boolean;
Wrapper: React.FC<{ children: React.ReactNode }>;
}

const ConditionalWrapper = ({ condition, Wrapper, children }: ConditionalWrapperProps) => {
return condition ? <Wrapper>{children}</Wrapper> : <>{children}</>;
};

export default ConditionalWrapper;
2 changes: 1 addition & 1 deletion src/ui/components/TooltipHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Tooltip } from '@mui/material';
interface TooltipHintProps {
title: string;
shortcut?: string;
children: JSX.Element;
children: React.ReactElement<any, any>;
// Anything else for <Tooltip> you want to pass
[x: string]: any;
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/menus/CodeEditor/AICodeBlockParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function parseCodeBlocks(input: string): Array<string | JSX.Element> {
}
blocks.push(
<div
key={lastIndex}
// calculate height based on number of lines
style={{
height: `${Math.ceil(code.split('\n').length) * 19}px`,
Expand Down
8 changes: 8 additions & 0 deletions src/ui/menus/CodeEditor/AITab.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ai-streaming-output * {
overflow-anchor: none;
}

#ai-streaming-output-anchor {
overflow-anchor: auto;
height: 1px;
}
174 changes: 106 additions & 68 deletions src/ui/menus/CodeEditor/AITab.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useAuth0 } from '@auth0/auth0-react';
import { Send } from '@mui/icons-material';
import { Send, Stop } from '@mui/icons-material';
import { Avatar, CircularProgress, FormControl, IconButton, InputAdornment, OutlinedInput } from '@mui/material';
import { useState } from 'react';
import { useRef, useState } from 'react';
import apiClientSingleton from '../../../api-client/apiClientSingleton';
import { EditorInteractionState } from '../../../atoms/editorInteractionStateAtom';
import { CellEvaluationResult } from '../../../grid/computations/types';
import { colors } from '../../../theme/colors';
import { TooltipHint } from '../../components/TooltipHint';
import { AI } from '../../icons';
import { CodeBlockParser } from './AICodeBlockParser';
import ConditionalWrapper from '../../components/ConditionalWrapper';
import './AITab.css';

interface Props {
editorMode: EditorInteractionState['mode'];
Expand Down Expand Up @@ -47,10 +50,17 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
const [prompt, setPrompt] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [messages, setMessages] = useState<Message[]>([]);
const controller = useRef<AbortController>();
const { user } = useAuth0();

const abortPrompt = () => {
controller.current?.abort();
setLoading(false);
};

const submitPrompt = async () => {
if (loading) return;
controller.current = new AbortController();
setLoading(true);
const token = await apiClientSingleton.getAuth();
const updatedMessages = [...messages, { role: 'user', content: prompt }] as Message[];
Expand All @@ -60,15 +70,17 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
};
setMessages(updatedMessages);
setPrompt('');
try {
await fetch(`${apiClientSingleton.getAPIURL()}/ai/chat/stream`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request_body),
}).then((response) => {

await fetch(`${apiClientSingleton.getAPIURL()}/ai/chat/stream`, {
method: 'POST',
signal: controller.current.signal,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request_body),
})
.then((response) => {
if (response.status !== 200) {
if (response.status === 429) {
setMessages((old) => [
Expand Down Expand Up @@ -129,10 +141,16 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
}
return reader.read().then(processResult);
});
})
.catch((err) => {
// not sure what would cause this to happen
if (err.name !== 'AbortError') {
console.log(err);
return;
}
});
} catch (err: any) {
// not sure what would cause this to happen
}
// eslint-disable-next-line no-unreachable

setLoading(false);
};

Expand All @@ -148,7 +166,7 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
right: '1rem',
padding: '1rem 0 .5rem 1rem',
background: 'linear-gradient(0deg, rgba(255,255,255,1) 85%, rgba(255,255,255,0) 100%)',
zIndex: 1000000,
zIndex: 100,
}}
>
<FormControl fullWidth>
Expand All @@ -159,17 +177,36 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
setPrompt(event.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.key === 'Enter' && prompt.length > 0) {
submitPrompt();
}
}}
placeholder="Ask a question"
endAdornment={
<InputAdornment position="end">
{loading && <CircularProgress size="1.25rem" sx={{ mx: '1rem' }} />}
<IconButton size="small" color="primary" onClick={submitPrompt} edge="end" disabled={loading}>
<Send />
</IconButton>
{loading ? (
<TooltipHint title="Stop generating">
<IconButton size="small" color="primary" onClick={abortPrompt} edge="end">
<Stop />
</IconButton>
</TooltipHint>
) : (
<ConditionalWrapper
condition={prompt.length !== 0}
Wrapper={({ children }) => <TooltipHint title="Send">{children as React.ReactElement}</TooltipHint>}
>
<IconButton
size="small"
color="primary"
onClick={submitPrompt}
edge="end"
{...(prompt.length === 0 ? { disabled: true } : {})}
>
<Send />
</IconButton>
</ConditionalWrapper>
)}
</InputAdornment>
}
size="small"
Expand Down Expand Up @@ -203,7 +240,7 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
data-gramm_editor="false"
data-enable-grammarly="false"
>
{display_message.length === 0 && (
{display_message.length === 0 ? (
<div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '1rem' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 500, marginBottom: '1rem' }}>
Expand All @@ -214,55 +251,56 @@ export const AITab = ({ evalResult, editorMode, editorContent }: Props) => {
</div>
</div>
</div>
) : (
<div id="ai-streaming-output">
{display_message.map((message, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
borderTop: index !== 0 ? `1px solid ${colors.lightGray}` : 'none',
marginTop: '1rem',
paddingTop: index !== 0 ? '1rem' : '0',
paddingLeft: '1rem',
paddingRight: '1rem',
}}
>
{message.role === 'user' ? (
<Avatar
variant="rounded"
sx={{
bgcolor: colors.quadraticSecondary,
width: 24,
height: 24,
fontSize: '0.8rem',
marginBottom: '0.5rem',
}}
alt={user?.name}
src={user?.picture}
></Avatar>
) : (
<Avatar
variant="rounded"
sx={{
bgcolor: 'white',
width: 24,
height: 24,
fontSize: '0.8rem',
marginBottom: '0.5rem',
}}
alt="AI Assistant"
>
<AI sx={{ color: colors.languageAI }}></AI>
</Avatar>
)}
<span>{CodeBlockParser({ input: message.content })}</span>
</div>
))}
<div id="ai-streaming-output-anchor" key="ai-streaming-output-anchor" />
</div>
)}
{display_message.map((message, index) => {
return (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
borderTop: index !== 0 ? `1px solid ${colors.lightGray}` : 'none',
marginTop: '1rem',
paddingTop: index !== 0 ? '1rem' : '0',
paddingLeft: '1rem',
paddingRight: '1rem',
}}
>
{message.role === 'user' ? (
<Avatar
variant="rounded"
sx={{
bgcolor: colors.quadraticSecondary,
width: 24,
height: 24,
fontSize: '0.8rem',
marginBottom: '0.5rem',
}}
alt={user?.name}
src={user?.picture}
></Avatar>
) : (
<Avatar
variant="rounded"
sx={{
bgcolor: 'white',
width: 24,
height: 24,
fontSize: '0.8rem',
marginBottom: '0.5rem',
}}
alt="AI Assistant"
>
<AI sx={{ color: colors.languageAI }}></AI>
</Avatar>
)}
<span>{CodeBlockParser({ input: message.content })}</span>
</div>
);
})}
{}
</div>
</>
);
Expand Down

1 comment on commit 56bb6a5

@vercel
Copy link

@vercel vercel bot commented on 56bb6a5 May 5, 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-git-main-quadratic.vercel.app
quadratic-nu.vercel.app
quadratic-quadratic.vercel.app

Please sign in to comment.