Skip to content

Commit

Permalink
xef-dashboard basic API (#372)
Browse files Browse the repository at this point in the history
* Modify Ktor config to allow CORS auth

* Add API and typings for chat completions

* Add generic question page using API

* Add settings page and context

* Rename Features for Pages in the code organization

* Slight styling changes
  • Loading branch information
calvellido authored Sep 1, 2023
1 parent ad1c29c commit 3105b29
Show file tree
Hide file tree
Showing 25 changed files with 633 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ object Server {
server(factory = Netty, port = 8081, host = "0.0.0.0") {
install(CORS) {
allowNonSimpleContentTypes = true
allowHeader("Authorization")
anyHost()
}
install(ContentNegotiation) { json() }
Expand Down
2 changes: 1 addition & 1 deletion server/web/package-lock.json

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

3 changes: 1 addition & 2 deletions server/web/src/components/App/App.module.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
.container {
overflow-x: hidden;
margin-top: 64px;
margin-bottom: 56px;
padding-bottom: 56px;
}

.mainContainer {
/* background: red; */
padding: 1rem;
transition: margin 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.response {
margin-top: 1rem;
white-space: break-spaces;
}
198 changes: 198 additions & 0 deletions server/web/src/components/Pages/GenericQuestion/GenericQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { ChangeEvent, KeyboardEvent, useContext, useState } from 'react';
import {
Alert,
Box,
IconButton,
InputAdornment,
Snackbar,
TextField,
Typography,
} from '@mui/material';
import { SendRounded } from '@mui/icons-material';

import { HourglassLoader } from '@/components/HourglassLoader';

import { LoadingContext } from '@/state/Loading';
import { SettingsContext } from '@/state/Settings';

import {
ApiOptions,
EndpointsEnum,
apiConfigConstructor,
apiFetch,
defaultApiServer,
} from '@/utils/api';

import styles from './GenericQuestion.module.css';

const baseHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

const chatCompletionsBaseRequest: ChatCompletionsRequest = {
model: 'gpt-3.5-turbo-16k',
messages: [
{
role: 'user',
content: '',
name: 'USER',
},
],
temperature: 0.4,
top_p: 1.0,
n: 1,
max_tokens: 12847,
presence_penalty: 0.0,
frequency_penalty: 0.0,
logit_bias: {},
user: 'USER',
};

const chatCompletionsApiBaseOptions: ApiOptions = {
endpointServer: defaultApiServer,
endpointPath: EndpointsEnum.chatCompletions,
endpointValue: '',
requestOptions: {
method: 'POST',
headers: baseHeaders,
},
};

export function GenericQuestion() {
const [loading, setLoading] = useContext(LoadingContext);
const [settings] = useContext(SettingsContext);
const [prompt, setPrompt] = useState<string>('');
const [showAlert, setShowAlert] = useState<string>('');
const [responseMessage, setResponseMessage] =
useState<ChatCompletionMessage['content']>('');

const handleClick = async () => {
if (!loading) {
try {
setLoading(true);
console.group(`🖱️ Generic question form used:`);

const chatCompletionsRequest: ChatCompletionsRequest = {
...chatCompletionsBaseRequest,
messages: [
{
...chatCompletionsBaseRequest.messages[0],
content: prompt,
},
],
};
const chatCompletionsApiOptions: ApiOptions = {
...chatCompletionsApiBaseOptions,
body: JSON.stringify(chatCompletionsRequest),
requestOptions: {
...chatCompletionsApiBaseOptions.requestOptions,
headers: {
...chatCompletionsApiBaseOptions.requestOptions?.headers,
Authorization: `Bearer ${settings.apiKey}`,
},
},
};
const chatCompletionsApiConfig = apiConfigConstructor(
chatCompletionsApiOptions,
);
const chatCompletionResponse = await apiFetch<ChatCompletionsResponse>(
chatCompletionsApiConfig,
);
const { content } = chatCompletionResponse.choices[0].message;

setResponseMessage(content);

console.info(`Chat completions request completed`);
} catch (error) {
const userFriendlyError = `Chat completions request couldn't be completed`;
console.info(userFriendlyError);
setShowAlert(`
${userFriendlyError}, is the API key set?`);
} finally {
console.groupEnd();
setLoading(false);
}
}
};

const handleKey = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleClick();
}
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setPrompt(event.target.value);
};

const disabledButton = loading || !prompt.trim();

return (
<>
<Box
sx={{
my: 1,
}}>
<Typography variant="h4" gutterBottom>
Generic question
</Typography>
<Typography variant="body1" gutterBottom>
This is an example of a generic call to the xef-server API.
</Typography>
<Typography variant="body1">
Please check that you have your OpenAI key set in the Settings page.
Then ask any question in the form below:
</Typography>
</Box>
<Box
sx={{
my: 3,
}}>
<TextField
id="prompt-input"
placeholder="e.g: what's the meaning of life?"
hiddenLabel
multiline
maxRows={2}
value={prompt}
onChange={handleChange}
onKeyDown={handleKey}
disabled={loading}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="send prompt text"
color="primary"
disabled={disabledButton}
title="Send message"
onClick={handleClick}>
{loading ? <HourglassLoader /> : <SendRounded />}
</IconButton>
</InputAdornment>
),
}}
inputProps={{
cols: 40,
}}
/>
</Box>
{responseMessage && (
<>
<Typography variant="h6">Response:</Typography>
<Typography variant="body1" className={styles.response}>
{responseMessage}
</Typography>
</>
)}
<Snackbar
open={!!showAlert}
onClose={(_, reason) => reason !== 'clickaway' && setShowAlert('')}
autoHideDuration={3000}>
<Alert severity="error">{showAlert}</Alert>
</Snackbar>
</>
);
}
1 change: 1 addition & 0 deletions server/web/src/components/Pages/GenericQuestion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './GenericQuestion';
57 changes: 57 additions & 0 deletions server/web/src/components/Pages/SettingsPage/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ChangeEvent, useContext, useState } from 'react';
import { Box, Button, TextField, Typography } from '@mui/material';
import { SaveRounded } from '@mui/icons-material';

import { SettingsContext } from '@/state/Settings';

export function SettingsPage() {
const [settings, setSettings] = useContext(SettingsContext);
const [apiKeyInput, setApiKeyInput] = useState<string>(settings.apiKey || '');

const handleSaving = () => {
setSettings((settings) => ({ ...settings, apiKey: apiKeyInput }));
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setApiKeyInput(event.target.value);
};

const disabledButton = settings.apiKey === apiKeyInput?.trim();

return (
<>
<Box
sx={{
my: 1,
}}>
<Typography variant="h4" gutterBottom>
Settings
</Typography>
<Typography variant="body1">These are xef-server settings.</Typography>
</Box>
<Box
sx={{
my: 3,
}}>
<TextField
id="api-key-input"
label="OpenAI API key"
value={apiKeyInput}
onChange={handleChange}
size="small"
sx={{
width: { xs: '100%', sm: 550 },
}}
/>
</Box>
<Button
onClick={handleSaving}
variant="contained"
startIcon={<SaveRounded />}
disableElevation
disabled={disabledButton}>
<Typography variant="button">Save settings</Typography>
</Button>
</>
);
}
1 change: 1 addition & 0 deletions server/web/src/components/Pages/SettingsPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './SettingsPage';
10 changes: 10 additions & 0 deletions server/web/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ export function Sidebar({ drawerWidth, open }: SidebarProps) {
<ListItemText primary="Feature Two" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={RouterLink} to="generic-question">
<ListItemText primary="Generic question" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={RouterLink} to="settings">
<ListItemText primary="Settings" />
</ListItemButton>
</ListItem>
</List>
</Box>
</Box>
Expand Down
2 changes: 1 addition & 1 deletion server/web/src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ html {

html,
body {
height: 100%;
height: calc(100% - 64px);
margin: 0;
}

Expand Down
23 changes: 18 additions & 5 deletions server/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { CssBaseline, StyledEngineProvider } from '@mui/material';
import { ThemeProvider } from '@emotion/react';

import { App } from '@/components/App';
import { Root } from '@/components/Features/Root';
import { ErrorPage } from '@/components/ErrorPage';
import { FeatureOne } from '@/components/Features/FeatureOne';
import { FeatureTwo } from '@/components/Features/FeatureTwo';
import { Root } from '@/components/Pages/Root';
import { ErrorPage } from '@/components/Pages/ErrorPage';
import { FeatureOne } from '@/components/Pages/FeatureOne';
import { FeatureTwo } from '@/components/Pages/FeatureTwo';
import { GenericQuestion } from '@/components/Pages/GenericQuestion';
import { SettingsPage } from '@/components/Pages/SettingsPage';

import { LoadingProvider } from '@/state/Loading';
import { SettingsProvider } from '@/state/Settings';

import { theme } from '@/styles/theme';

Expand All @@ -34,6 +37,14 @@ const router = createBrowserRouter([
path: '2',
element: <FeatureTwo />,
},
{
path: 'generic-question',
element: <GenericQuestion />,
},
{
path: 'settings',
element: <SettingsPage />,
},
],
},
]);
Expand All @@ -44,7 +55,9 @@ createRoot(document.getElementById('root') as HTMLElement).render(
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<LoadingProvider>
<RouterProvider router={router} />
<SettingsProvider>
<RouterProvider router={router} />
</SettingsProvider>
</LoadingProvider>
</ThemeProvider>
</StyledEngineProvider>
Expand Down
Loading

0 comments on commit 3105b29

Please sign in to comment.