WIP: Update UI to match Official Style; Vision and Assistants 👷🏽 (#1190)

* wip: initial client side code

* wip: initial api code

* refactor: export query keys from own module, export assistant hooks

* refactor(SelectDropDown): more customization via props

* feat: create Assistant and render real Assistants

* refactor: major refactor of UI components to allow multi-chat, working alongside CreationPanel

* refactor: move assistant routes to own directory

* fix(CreationHeader): state issue with assistant select

* refactor: style changes for form, fix setSiblingIdx from useChatHelpers to use latestMessageParentId, fix render issue with ChatView and change location

* feat: parseCompactConvo: begin refactor of slimmer JSON payloads between client/api

* refactor(endpoints): add assistant endpoint, also use EModelEndpoint as much as possible

* refactor(useGetConversationsQuery): use object to access query data easily

* fix(MultiMessage): react warning of bad state set, making use of effect during render (instead of useEffect)

* fix(useNewConvo): use correct atom key (index instead of convoId) for reset latestMessageFamily

* refactor: make routing navigation/conversation change simpler

* chore: add removeNullishValues for smaller payloads, remove unused fields, setup frontend pinging of assistant endpoint

* WIP: initial complete assistant run handling

* fix: CreationPanel form correctly setting internal state

* refactor(api/assistants/chat): revise functions to working run handling strategy

* refactor(UI): initial major refactor of ChatForm and options

* feat: textarea hook

* refactor: useAuthRedirect hook and change directory name

* feat: add ChatRoute (/c/), make optionsBar absolute and change on textarea height, add temp header

* feat: match new toggle Nav open button to ChatGPT's

* feat: add OpenAI custom classnames

* feat: useOriginNavigate

* feat: messages loading view

* fix: conversation navigation and effects

* refactor: make toggle change nav opacity

* WIP: new endpoint menu

* feat: NewEndpointsMenu complete

* fix: ensure set key dialog shows on endpoint change, and new conversation resets messages

* WIP: textarea styling fix, add temp footer, create basic file handling component

* feat: image file handling (UI)

* feat: PopOver and ModelSelect in Header, remove GenButtons

* feat: drop file handling

* refactor: bug fixes
use SSE at route level
add opts to useOriginNavigate
delay render of unfinishedMessage to avoid flickering
pass params (convoId) to chatHelpers to set messages query data based on param when the route is new (fixes can't continue convo on /new/)
style(MessagesView): matches height to official
fix(SSE): pass paramId and invalidate convos
style(Message): make bg uniform

* refactor(useSSE): setStorage within setConversation updates

* feat: conversationKeysAtom, allConversationsSelector, update convos query data on created message (if new), correctly handle convo deletion (individual)

* feat: add popover select dropdowns to allow options in header while allowing horizontal scroll for mobile

* style(pluginsSelect): styling changes

* refactor(NewEndpointsMenu): make UI components modular

* feat: Presets complete

* fix: preset editing, make by index

* fix: conversations not setting on inital navigation, fix getMessages() based on query param

* fix: changing preset no longer resets latestMessage

* feat: useOnClickOutside for OptionsPopover and fix bug that causes selection of preset when deleting

* fix: revert /chat/ switchToConvo, also use NewDeleteButton in Convo

* fix: Popover correctly closes on close Popover button using custom condition for useOnClickOutside

* style: new message and nav styling

* style: hover/sibling buttons and preset menu scrolling

* feat: new convo header button

* style(Textarea): minor style changes to textarea buttons

* feat: stop/continue generating and hide hoverbuttons when submitting

* feat: compact AI Provider schemas to make json payloads and db saves smaller

* style: styling changes for consistency on chat route

* fix: created usePresetIndexOptions to prevent bugs between /c/ and /chat/ routes when editing presets, removed redundant code from the new dialog

* chore: make /chat/ route default for now since we still lack full image support
This commit is contained in:
Danny Avila 2023-11-16 10:42:24 -05:00 committed by GitHub
parent adbeb46399
commit bac1fb67d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
171 changed files with 8380 additions and 468 deletions

View file

@ -111,7 +111,7 @@ export default function Conversation({ conversation, retainView }) {
if (currentConversation?.conversationId !== conversationId) {
aProps.className =
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-gray-800 hover:pr-4';
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-gray-900 hover:pr-4';
}
return (
@ -149,7 +149,7 @@ export default function Conversation({ conversation, retainView }) {
/>
</div>
) : (
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-gray-900 group-hover:from-gray-700/70" />
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-black group-hover:from-gray-900" />
)}
</a>
);

View file

@ -1,4 +1,6 @@
import Convo from './Convo';
import Conversation from './Conversation';
import { useLocation } from 'react-router-dom';
import { TConversation } from 'librechat-data-provider';
export default function Conversations({
@ -8,13 +10,22 @@ export default function Conversations({
conversations: TConversation[];
moveToTop: () => void;
}) {
const location = useLocation();
const { pathname } = location;
const ConvoItem = pathname.includes('chat') ? Conversation : Convo;
return (
<>
{conversations &&
conversations.length > 0 &&
conversations.map((convo: TConversation) => {
conversations.map((convo: TConversation, i) => {
return (
<Conversation key={convo.conversationId} conversation={convo} retainView={moveToTop} />
<ConvoItem
key={convo.conversationId}
conversation={convo}
retainView={moveToTop}
i={i}
/>
);
})}
</>

View file

@ -0,0 +1,142 @@
import { useRecoilValue } from 'recoil';
import { useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { useUpdateConversationMutation } from 'librechat-data-provider';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { useConversations, useNavigateToConvo } from '~/hooks';
import { MinimalIcon } from '~/components/Endpoints';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import DeleteButton from './NewDeleteButton';
import RenameButton from './RenameButton';
import store from '~/store';
type KeyEvent = KeyboardEvent<HTMLInputElement>;
export default function Conversation({ conversation, retainView, i }) {
const { conversationId: currentConvoId } = useParams();
const activeConvos = useRecoilValue(store.allConversationsSelector);
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
const { refreshConversations } = useConversations();
const { navigateToConvo } = useNavigateToConvo();
const { showToast } = useToastContext();
const { conversationId, title } = conversation;
const inputRef = useRef<HTMLInputElement | null>(null);
const [titleInput, setTitleInput] = useState(title);
const [renaming, setRenaming] = useState(false);
const clickHandler = async () => {
if (currentConvoId === conversationId) {
return;
}
// set document title
document.title = title;
// set conversation to the new conversation
if (conversation?.endpoint === 'gptPlugins') {
const lastSelectedTools = JSON.parse(localStorage.getItem('lastSelectedTools') ?? '') || [];
navigateToConvo({ ...conversation, tools: lastSelectedTools });
} else {
navigateToConvo(conversation);
}
};
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setTitleInput(title);
setRenaming(true);
setTimeout(() => {
if (!inputRef.current) {
return;
}
inputRef.current.focus();
}, 25);
};
const onRename = (e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
updateConvoMutation.mutate(
{ conversationId, title: titleInput },
{
onSuccess: () => refreshConversations(),
onError: () => {
setTitleInput(title);
showToast({
message: 'Failed to rename conversation',
severity: NotificationSeverity.ERROR,
showIcon: true,
});
},
},
);
};
const icon = MinimalIcon({
size: 20,
endpoint: conversation.endpoint,
model: conversation.model,
error: false,
className: 'mr-0',
isCreatedByUser: false,
});
const handleKeyDown = (e: KeyEvent) => {
if (e.key === 'Enter') {
onRename(e);
}
};
const aProps = {
className:
'animate-flash group relative flex cursor-pointer items-center gap-3 break-all rounded-md bg-gray-900 py-3 px-3 pr-14 hover:bg-gray-900',
};
const activeConvo =
currentConvoId === conversationId ||
(i === 0 && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new');
if (!activeConvo) {
aProps.className =
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-gray-900 hover:pr-4';
}
return (
<a data-testid="convo-item" onClick={() => clickHandler()} {...aProps}>
{icon}
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all">
{renaming === true ? (
<input
ref={inputRef}
type="text"
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
) : (
title
)}
</div>
{activeConvo ? (
<div className="visible absolute right-1 z-10 flex text-gray-400">
<RenameButton renaming={renaming} onRename={onRename} renameHandler={renameHandler} />
<DeleteButton
conversationId={conversationId}
retainView={retainView}
renaming={renaming}
title={title}
/>
</div>
) : (
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-black group-hover:from-gray-900" />
)}
</a>
);
}

View file

@ -1,17 +1,15 @@
import TrashIcon from '../svg/TrashIcon';
import CrossIcon from '../svg/CrossIcon';
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { useDeleteConversationMutation } from 'librechat-data-provider';
import { Dialog, DialogTrigger, Label } from '~/components/ui/';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useLocalize, useConversations, useConversation } from '~/hooks';
import store from '~/store';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { TrashIcon, CrossIcon } from '~/components/svg';
export default function DeleteButton({ conversationId, renaming, retainView, title }) {
const localize = useLocalize();
const currentConversation = useRecoilValue(store.conversation) || {};
const { newConversation } = useConversation();
const { refreshConversations } = useConversations();
const { conversationId: currentConvoId } = useParams();
const deleteConvoMutation = useDeleteConversationMutation(conversationId);
const confirmDelete = () => {
@ -19,9 +17,7 @@ export default function DeleteButton({ conversationId, renaming, retainView, tit
{ conversationId, source: 'button' },
{
onSuccess: () => {
if (
(currentConversation as { conversationId?: string }).conversationId == conversationId
) {
if (currentConvoId == conversationId) {
newConversation();
}

View file

@ -0,0 +1,59 @@
import { useParams } from 'react-router-dom';
import { useDeleteConversationMutation } from 'librechat-data-provider';
import { useLocalize, useConversations, useNewConvo } from '~/hooks';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { TrashIcon, CrossIcon } from '~/components/svg';
export default function DeleteButton({ conversationId, renaming, retainView, title }) {
const localize = useLocalize();
// TODO: useNewConvo uses indices so we need to update global index state on every switch to Convo
const { newConversation } = useNewConvo();
const { refreshConversations } = useConversations();
const { conversationId: currentConvoId } = useParams();
const deleteConvoMutation = useDeleteConversationMutation(conversationId);
const confirmDelete = () => {
deleteConvoMutation.mutate(
{ conversationId, source: 'button' },
{
onSuccess: () => {
if (currentConvoId == conversationId) {
newConversation();
}
refreshConversations();
retainView();
},
},
);
};
return (
<Dialog>
<DialogTrigger asChild>
<button className="p-1 hover:text-white">{renaming ? <CrossIcon /> : <TrashIcon />}</button>
</DialogTrigger>
<DialogTemplate
title={localize('com_ui_delete_conversation')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
{localize('com_ui_delete_conversation_confirm')} <strong>{title}</strong>
</Label>
</div>
</div>
</>
}
selection={{
selectHandler: confirmDelete,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</Dialog>
);
}

View file

@ -1,11 +1,10 @@
import React, { ReactElement } from 'react';
import RenameIcon from '../svg/RenameIcon';
import CheckMark from '../svg/CheckMark';
import type { MouseEvent, ReactElement } from 'react';
import { RenameIcon, CheckMark } from '~/components/svg';
interface RenameButtonProps {
renaming: boolean;
renameHandler: () => void;
onRename: () => void;
renameHandler: (e: MouseEvent<HTMLButtonElement>) => void;
onRename: (e: MouseEvent<HTMLButtonElement>) => void;
twcss?: string;
}