Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AI ui/ux enhancements #466

Merged
merged 13 commits into from
May 5, 2023
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sentence casing tweaks because i need to re-trigger a build ha


Check out our open roles -> [careers.quadratichq.com](https://careers.quadratichq.com)
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;
}
170 changes: 102 additions & 68 deletions src/ui/menus/CodeEditor/AITab.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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 './AITab.css';

interface Props {
editorMode: EditorInteractionState['mode'];
Expand Down Expand Up @@ -47,10 +49,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 +69,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 +140,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 +165,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 +176,33 @@ 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>
) : (
<TooltipHint title="Send">
<IconButton
size="small"
color="primary"
onClick={submitPrompt}
edge="end"
disabled={prompt.length === 0}
>
<Send />
</IconButton>
</TooltipHint>
)}
</InputAdornment>
}
size="small"
Expand Down Expand Up @@ -203,7 +236,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 +247,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
Loading