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

Implement View Code on the Model page in the UI and small improvements #1825

Merged
merged 4 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/assets/icons/source.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"info": "Info",
"stop": "Stop",
"abort": "Abort",
"close": "Close",
"clearFilter": "Clear filter",
"server_error": "Server error: {{error}}",
"login": "Sign in",
Expand Down Expand Up @@ -405,7 +406,9 @@
"message_placeholder": "Enter your question",
"chat_empty_title": "No messages yet",
"chat_empty_message": "Please start a chat",
"run_name": "Run name"
"run_name": "Run name",
"view_code": "View code",
"view_code_description": "You can use the following code to start integrating your current prompt and settings into your application."
}
},

Expand Down
245 changes: 178 additions & 67 deletions frontend/src/pages/Models/Details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import reactStringReplace from 'react-string-replace';
import { get as _get } from 'lodash';
import cn from 'classnames';
import { format } from 'date-fns';
import OpenAI from 'openai';

import {
Expand All @@ -20,22 +18,26 @@ import {
Header,
ListEmptyMessage,
Loader,
Modal,
NavigateLink,
StatusIndicator,
SelectCSD,
SelectCSDProps,
SpaceBetween,
} from 'components';

import { useAppSelector, useBreadcrumbs, useNotifications } from 'hooks';
import { getExtendedModelFromRun, getStatusIconType } from 'libs/run';
import { getExtendedModelFromRun } from 'libs/run';
import { ROUTES } from 'routes';
import { useGetRunQuery } from 'services/run';

import { selectAuthToken } from 'App/slice';

import { DATE_TIME_FORMAT } from '../../../consts';
import { copyToClipboard } from '../../../libs';

import { IModelExtended } from '../List/types';
import { FormValues, Message, Role } from './types';

import { ReactComponent as SourceIcon } from 'assets/icons/source.svg';
import css from './styles.module.scss';

const MESSAGE_ROLE_MAP: Record<Role, string> = {
Expand All @@ -45,15 +47,23 @@ const MESSAGE_ROLE_MAP: Record<Role, string> = {
assistant: 'Assistant',
};

const VIEW_CODE_TYPE_OPTIONS = [
{ label: 'Python', value: 'python' },
{ label: 'Curl', value: 'curl' },
];

export const ModelDetails: React.FC = () => {
const { t } = useTranslation();
const token = useAppSelector(selectAuthToken);
const [messages, setMessages] = useState<Message[]>([]);
const [viewCodeVisible, setViewCodeVisible] = useState<boolean>(false);
const [selectedCode, setSelectedCode] = useState<SelectCSDProps.Option>(VIEW_CODE_TYPE_OPTIONS[0]);
const [loading, setLoading] = useState<boolean>(false);
const params = useParams();
const paramProjectName = params.projectName ?? '';
const paramRunName = params.runName ?? '';
const openai = useRef<OpenAI>();
const textAreaRef = useRef<HTMLDivElement>(null);
const chatList = useRef<HTMLElement>();
const [pushNotification] = useNotifications();

Expand Down Expand Up @@ -114,7 +124,9 @@ export const ModelDetails: React.FC = () => {
scrollChatToBottom();
}, [messageForShowing]);

const { handleSubmit, control, setValue } = useForm<FormValues>();
const { handleSubmit, control, setValue, watch } = useForm<FormValues>();

const messageText = watch('message');

const sendRequest = async (messages: Message[]) => {
if (!openai.current) return Promise.reject('Model not found');
Expand Down Expand Up @@ -208,7 +220,7 @@ export const ModelDetails: React.FC = () => {
[...matches].forEach((l) => l && languages.push(l.replace(/^```/, '')));
}

const replacedStrings = reactStringReplace(content, PATTERN, (match, i) => {
const replacedStrings = reactStringReplace(content, PATTERN, (match) => {
if (!match) {
return '';
}
Expand All @@ -233,6 +245,62 @@ export const ModelDetails: React.FC = () => {
);
};

const onChangeMessage = () => {
if (!textAreaRef.current) return;

const textAreaElement = textAreaRef.current.querySelector('textarea');

if (!textAreaElement) return;

textAreaElement.style.height = 'auto';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
};

const onKeyDown = (event) => {
if (event?.detail?.keyCode === 13 && !event?.detail?.ctrlKey) {
handleSubmit(onSubmit)();
} else if (event?.detail?.keyCode === 13 && event?.detail?.ctrlKey) {
event.preventDefault();
setValue('message', messageText + '\n');
setTimeout(onChangeMessage, 0);
}
};

const pythonCode = `from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
model="${modelData?.name ?? ''}",
messages=[],
stream=True,
max_tokens=512,
response_format={
"type": "text"
}
)`;

const curlCode = `curl https://api.openai.com/v1/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer $OPENAI_API_KEY" \\
-d '{
"model": "${modelData?.name ?? ''}",
"messages": [],
"stream": true,
"max_tokens": 512,
"response_format": {
"type": "text"
}
}'`;

const onCopyCode = () => {
switch (selectedCode.value) {
case VIEW_CODE_TYPE_OPTIONS[0].value:
return copyToClipboard(pythonCode);
case VIEW_CODE_TYPE_OPTIONS[1].value:
return copyToClipboard(curlCode);
}
};

return (
<ContentLayout
header={
Expand All @@ -252,76 +320,119 @@ export const ModelDetails: React.FC = () => {
)}

{modelData && (
<div className={css.modelDetailsLayout}>
<div className={css.general}>
<Container header={<Header variant="h2">{t('common.general')}</Header>}>
<ColumnLayout columns={4} variant="text-grid">
<div>
<Box variant="awsui-key-label">{t('models.details.run_name')}</Box>
<div>{modelData.run_name}</div>
</div>
<>
<div className={css.modelDetailsLayout}>
<div className={css.general}>
<Container header={<Header variant="h2">{t('common.general')}</Header>}>
<ColumnLayout columns={4} variant="text-grid">
<div>
<Box variant="awsui-key-label">{t('models.details.run_name')}</Box>
<div>{modelData.run_name}</div>
</div>

<div>
<Box variant="awsui-key-label">{t('models.model_name')}</Box>
<div>{modelData.name}</div>
</div>

<div>
<Box variant="awsui-key-label">{t('models.type')}</Box>
<div>{modelData.type}</div>
</div>
</ColumnLayout>
</Container>
</div>

<div>
<Box variant="awsui-key-label">{t('models.model_name')}</Box>
<div>{modelData.name}</div>
</div>
<form className={css.modelForm} onSubmit={handleSubmit(onSubmit)}>
<div className={css.buttons}>
<Button iconSvg={<SourceIcon />} disabled={loading} onClick={() => setViewCodeVisible(true)} />

<div>
<Box variant="awsui-key-label">{t('models.type')}</Box>
<div>{modelData.type}</div>
</div>
</ColumnLayout>
</Container>
</div>
<Button iconName="remove" disabled={loading || !messages.length} onClick={clearChat} />
</div>

<form className={css.modelForm} onSubmit={handleSubmit(onSubmit)}>
<div className={css.clear}>
<Button iconName="remove" disabled={loading || !messages.length} onClick={clearChat} />
</div>
<aside className={css.side}>
<FormTextarea
disabled={loading}
label={t('models.details.instructions')}
constraintText={t('models.details.instructions_description')}
control={control}
name="instructions"
/>
</aside>

<div className={css.chat} ref={chatList}>
{!messageForShowing.length && (
<ListEmptyMessage
title={t('models.details.chat_empty_title')}
message={t('models.details.chat_empty_message')}
/>
)}

{messageForShowing.map((message, index) => (
<div key={index} className={cn(css.message, css[message.role])}>
<Box variant="h4">
{MESSAGE_ROLE_MAP[message.role as keyof typeof MESSAGE_ROLE_MAP]}
</Box>
{renderMessageBody(message.content || '...')}
</div>
))}
</div>

<aside className={css.side}>
<FormTextarea
disabled={loading}
label={t('models.details.instructions')}
constraintText={t('models.details.instructions_description')}
control={control}
name="instructions"
/>
</aside>

<div className={css.chat} ref={chatList}>
{!messageForShowing.length && (
<ListEmptyMessage
title={t('models.details.chat_empty_title')}
message={t('models.details.chat_empty_message')}
<div ref={textAreaRef} className={css.messageForm}>
<FormTextarea
stretch
placeholder={t('models.details.message_placeholder')}
control={control}
disabled={loading}
name="message"
onKeyDown={onKeyDown}
onChange={onChangeMessage}
/>
)}

{messageForShowing.map((message, index) => (
<div key={index} className={cn(css.message, css[message.role])}>
<Box variant="h4">{MESSAGE_ROLE_MAP[message.role as keyof typeof MESSAGE_ROLE_MAP]}</Box>
{renderMessageBody(message.content || '...')}
<div className={css.buttons}>
<Button variant="primary" disabled={loading}>
{t('common.send')}
</Button>
</div>
))}
</div>

<div className={css.messageForm}>
<FormTextarea
stretch
placeholder={t('models.details.message_placeholder')}
control={control}
disabled={loading}
name="message"
/>
</div>
</form>
</div>

<div className={css.buttons}>
<Button variant="primary" disabled={loading}>
{t('common.send')}
<Modal
visible={viewCodeVisible}
header={t('models.details.view_code')}
size="large"
footer={
<Box float="right">
<Button variant="normal" onClick={() => setViewCodeVisible(false)}>
{t('common.close')}
</Button>
</Box>
}
onDismiss={() => setViewCodeVisible(false)}
>
<SpaceBetween size="m" direction="vertical">
<Box>{t('models.details.view_code_description')}</Box>

<div className={css.viewCodeControls}>
<SelectCSD
options={VIEW_CODE_TYPE_OPTIONS}
selectedOption={selectedCode}
expandToViewport={true}
onChange={(event) => {
setSelectedCode(event.detail.selectedOption);
}}
/>

<Button iconName="copy" onClick={onCopyCode}></Button>
</div>
</div>
</form>
</div>

{selectedCode.value === VIEW_CODE_TYPE_OPTIONS[0].value && <Code>{pythonCode}</Code>}

{selectedCode.value === VIEW_CODE_TYPE_OPTIONS[1].value && <Code>{curlCode}</Code>}
</SpaceBetween>
</Modal>
</>
)}
</ContentLayout>
);
Expand Down
Loading