make all chat area components tabbable (accessibility) (#246)

* make all chat area components tabbable

* align message role description

* remove inline styles on icons

* remove inline styles on icons
This commit is contained in:
Brad Ullman 2023-03-28 01:35:57 -07:00 committed by GitHub
parent 5d31947ab9
commit a78a8c4a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 83 additions and 96 deletions

View File

@ -217,16 +217,17 @@ export const Chat: FC<Props> = memo(
<> <>
<div className="flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"> <div className="flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200">
{t('Model')}: {conversation.model.name} {t('Model')}: {conversation.model.name}
<IconSettings <button
className="ml-2 cursor-pointer hover:opacity-50" className="ml-2 cursor-pointer hover:opacity-50"
onClick={handleSettings} onClick={handleSettings}
size={18} >
/> <IconSettings size={18} />
<IconClearAll </button>
<button
className="ml-2 cursor-pointer hover:opacity-50" className="ml-2 cursor-pointer hover:opacity-50"
onClick={onClearAll} onClick={onClearAll}>
size={18} <IconClearAll size={18} />
/> </button>
</div> </div>
{showSettings && ( {showSettings && (
<div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">

View File

@ -237,28 +237,26 @@ export const ChatInput: FC<Props> = ({
<div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl"> <div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl">
{messageIsStreaming && ( {messageIsStreaming && (
<button <button
className="absolute top-2 left-0 right-0 mx-auto mt-2 w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0" className="absolute left-0 right-0 mx-auto mt-2 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
onClick={handleStopConversation} onClick={handleStopConversation}
> >
<IconPlayerStop size={16} className="mb-[2px] inline-block" />{' '} <IconPlayerStop size={16} /> {t('Stop Generating')}
{t('Stop Generating')}
</button> </button>
)} )}
{!messageIsStreaming && !conversationIsEmpty && ( {!messageIsStreaming && !conversationIsEmpty && (
<button <button
className="absolute left-0 right-0 mx-auto mt-2 w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0" className="absolute left-0 right-0 mx-auto mt-2 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
onClick={onRegenerate} onClick={onRegenerate}
> >
<IconRepeat size={16} className="mb-[2px] inline-block" />{' '} <IconRepeat size={16} /> {t('Regenerate response')}
{t('Regenerate response')}
</button> </button>
)} )}
<div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4 md:py-3 md:pl-4"> <div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className="m-0 w-full resize-none border-0 bg-transparent p-0 pr-8 pl-2 text-black outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:text-white md:pl-0" className="m-0 w-full resize-none border-0 bg-transparent p-0 pr-8 pl-2 text-black dark:bg-transparent dark:text-white py-2 md:py-3 md:pl-4"
style={{ style={{
resize: 'none', resize: 'none',
bottom: `${textareaRef?.current?.scrollHeight}px`, bottom: `${textareaRef?.current?.scrollHeight}px`,
@ -279,12 +277,11 @@ export const ChatInput: FC<Props> = ({
onChange={handleChange} onChange={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
<button <button
className="absolute right-3 rounded-sm p-1 text-neutral-800 hover:bg-neutral-200 hover:text-neutral-900 focus:outline-none dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200" className="absolute right-2 top-2 rounded-sm p-1 text-neutral-800 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200 opacity-60"
onClick={handleSend} onClick={handleSend}
> >
<IconSend size={16} className="opacity-60" /> <IconSend size={18} />
</button> </button>
{showPromptList && prompts.length > 0 && ( {showPromptList && prompts.length > 0 && (

View File

@ -2,12 +2,12 @@ import { Message } from '@/types/chat';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { FC, memo, useEffect, useRef, useState } from 'react'; import { FC, memo, useEffect, useRef, useState } from 'react';
import { IconCheck, IconCopy } from '@tabler/icons-react';
import rehypeMathjax from 'rehype-mathjax'; import rehypeMathjax from 'rehype-mathjax';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import { CodeBlock } from '../Markdown/CodeBlock'; import { CodeBlock } from '../Markdown/CodeBlock';
import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown'; import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown';
import { CopyButton } from './CopyButton';
interface Props { interface Props {
message: Message; message: Message;
@ -19,7 +19,6 @@ export const ChatMessage: FC<Props> = memo(
({ message, messageIndex, onEditMessage }) => { ({ message, messageIndex, onEditMessage }) => {
const { t } = useTranslation('chat'); const { t } = useTranslation('chat');
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
const [isHovering, setIsHovering] = useState<boolean>(false);
const [messageContent, setMessageContent] = useState(message.content); const [messageContent, setMessageContent] = useState(message.content);
const [messagedCopied, setMessageCopied] = useState(false); const [messagedCopied, setMessageCopied] = useState(false);
@ -79,11 +78,9 @@ export const ChatMessage: FC<Props> = memo(
: 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100' : 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100'
}`} }`}
style={{ overflowWrap: 'anywhere' }} style={{ overflowWrap: 'anywhere' }}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
> >
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="min-w-[40px] font-bold"> <div className="min-w-[40px] font-bold text-right">
{message.role === 'assistant' ? t('AI') : t('You')}: {message.role === 'assistant' ? t('AI') : t('You')}:
</div> </div>
@ -94,7 +91,7 @@ export const ChatMessage: FC<Props> = memo(
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className="w-full resize-none whitespace-pre-wrap border-none outline-none dark:bg-[#343541]" className="w-full resize-none whitespace-pre-wrap border-none dark:bg-[#343541]"
value={messageContent} value={messageContent}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handlePressEnter} onKeyDown={handlePressEnter}
@ -133,24 +130,44 @@ export const ChatMessage: FC<Props> = memo(
</div> </div>
)} )}
{(isHovering || window.innerWidth < 640) && !isEditing && ( {(window.innerWidth < 640 || !isEditing) && (
<button <button
className={`absolute ${ className={`absolute translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300 ${
window.innerWidth < 640 window.innerWidth < 640
? 'right-3 bottom-1' ? 'right-3 bottom-1'
: 'right-0 top-[26px]' : 'right-0 top-[26px]'
}`} }
`}
onClick={toggleEditing}
> >
<IconEdit <IconEdit size={20} />
size={20}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
onClick={toggleEditing}
/>
</button> </button>
)} )}
</div> </div>
) : ( ) : (
<> <>
<div
className={`absolute ${
window.innerWidth < 640
? 'right-3 bottom-1'
: 'right-0 top-[26px] m-0'
}`}
>
{messagedCopied ? (
<IconCheck
size={20}
className="text-green-500 dark:text-green-400"
/>
) : (
<button
className="translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300"
onClick={copyOnClick}
>
<IconCopy size={20} />
</button>
)}
</div>
<MemoizedReactMarkdown <MemoizedReactMarkdown
className="prose dark:prose-invert" className="prose dark:prose-invert"
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
@ -197,13 +214,6 @@ export const ChatMessage: FC<Props> = memo(
> >
{message.content} {message.content}
</MemoizedReactMarkdown> </MemoizedReactMarkdown>
{(isHovering || window.innerWidth < 640) && (
<CopyButton
messagedCopied={messagedCopied}
copyOnClick={copyOnClick}
/>
)}
</> </>
)} )}
</div> </div>

View File

@ -1,25 +0,0 @@
import { IconCheck, IconCopy } from '@tabler/icons-react';
import { FC } from 'react';
type Props = {
messagedCopied: boolean;
copyOnClick: () => void;
};
export const CopyButton: FC<Props> = ({ messagedCopied, copyOnClick }) => (
<button
className={`absolute ${
window.innerWidth < 640 ? 'right-3 bottom-1' : 'right-0 top-[26px] m-0'
}`}
>
{messagedCopied ? (
<IconCheck size={20} className="text-green-500 dark:text-green-400" />
) : (
<IconCopy
size={20}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
onClick={copyOnClick}
/>
)}
</button>
);

View File

@ -17,7 +17,7 @@ export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => {
</label> </label>
<div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white"> <div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white">
<select <select
className="w-full bg-transparent p-2 outline-0" className="w-full bg-transparent p-2"
placeholder={t('Select a model') || ''} placeholder={t('Select a model') || ''}
value={model.id} value={model.id}
onChange={(e) => { onChange={(e) => {

View File

@ -19,13 +19,7 @@ export const PromptList: FC<Props> = ({
return ( return (
<ul <ul
ref={promptListRef} ref={promptListRef}
className="z-10 w-full rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]" className="z-10 w-full rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] max-h-52 overflow-scroll"
style={{
width: 'calc(100% - 48px)',
bottom: '100%',
marginBottom: '4px',
maxHeight: '200px',
}}
> >
{prompts.map((prompt, index) => ( {prompts.map((prompt, index) => (
<li <li

View File

@ -14,10 +14,10 @@ export const Regenerate: FC<Props> = ({ onRegenerate }) => {
{t('Sorry, there was an error.')} {t('Sorry, there was an error.')}
</div> </div>
<button <button
className="flex h-12 w-full items-center justify-center rounded-lg border border-b-neutral-300 bg-neutral-100 text-sm font-semibold text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200" className="flex h-12 gap-2 w-full items-center justify-center rounded-lg border border-b-neutral-300 bg-neutral-100 text-sm font-semibold text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"
onClick={onRegenerate} onClick={onRegenerate}
> >
<IconRefresh className="mr-2" /> <IconRefresh />
<div>{t('Regenerate response')}</div> <div>{t('Regenerate response')}</div>
</button> </button>
</div> </div>

View File

@ -196,7 +196,7 @@ export const SystemPrompt: FC<Props> = ({
</label> </label>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className="w-full rounded-lg border border-neutral-200 bg-transparent px-4 py-3 text-neutral-900 focus:outline-none dark:border-neutral-600 dark:text-neutral-100" className="w-full rounded-lg border border-neutral-200 bg-transparent px-4 py-3 text-neutral-900 dark:border-neutral-600 dark:text-neutral-100"
style={{ style={{
resize: 'none', resize: 'none',
bottom: `${textareaRef?.current?.scrollHeight}px`, bottom: `${textareaRef?.current?.scrollHeight}px`,

View File

@ -194,11 +194,9 @@ export const Chatbar: FC<Props> = ({
/> />
</div> </div>
) : ( ) : (
<div className="mt-8 select-none text-center text-white opacity-50"> <div className="flex flex-col gap-3 items-center text-sm leading-normal mt-8 text-white opacity-50">
<IconMessagesOff className="mx-auto mb-3" /> <IconMessagesOff />
<span className="text-[14px] leading-normal">
{t('No conversations.')} {t('No conversations.')}
</span>
</div> </div>
)} )}
</div> </div>

View File

@ -64,18 +64,18 @@ export const CodeBlock: FC<Props> = memo(({ language, value }) => {
<div className="flex items-center"> <div className="flex items-center">
<button <button
className="flex items-center rounded bg-none py-0.5 px-2 text-xs text-white focus:outline-none" className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
onClick={copyToClipboard} onClick={copyToClipboard}
> >
{isCopied ? ( {isCopied ? (
<IconCheck size={18} className="mr-1.5" /> <IconCheck size={18} />
) : ( ) : (
<IconClipboard size={18} className="mr-1.5" /> <IconClipboard size={18} />
)} )}
{isCopied ? t('Copied!') : t('Copy code')} {isCopied ? t('Copied!') : t('Copy code')}
</button> </button>
<button <button
className="flex items-center rounded bg-none py-0.5 pl-2 text-xs text-white focus:outline-none" className="flex items-center rounded bg-none p-1 text-xs text-white"
onClick={downloadAsFile} onClick={downloadAsFile}
> >
<IconDownload size={18} /> <IconDownload size={18} />

View File

@ -655,21 +655,24 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
onImportConversations={handleImportConversations} onImportConversations={handleImportConversations}
/> />
<IconArrowBarLeft <button
className="fixed top-5 left-[270px] z-50 h-7 w-7 cursor-pointer hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-[270px] sm:h-8 sm:w-8 sm:text-neutral-700" className="fixed top-5 left-[270px] z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-[270px] sm:h-8 sm:w-8 sm:text-neutral-700"
onClick={handleToggleChatbar} onClick={handleToggleChatbar}
/> >
<IconArrowBarLeft />
</button>
<div <div
onClick={handleToggleChatbar} onClick={handleToggleChatbar}
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden" className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
></div> ></div>
</div> </div>
) : ( ) : (
<IconArrowBarRight <button
className="fixed top-2.5 left-4 z-50 h-7 w-7 cursor-pointer text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-4 sm:h-8 sm:w-8 sm:text-neutral-700" className="fixed top-2.5 left-4 z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-4 sm:h-8 sm:w-8 sm:text-neutral-700"
onClick={handleToggleChatbar} onClick={handleToggleChatbar}
/> >
<IconArrowBarRight />
</button>
)} )}
<div className="flex flex-1"> <div className="flex flex-1">
@ -702,20 +705,24 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
onDeleteFolder={handleDeleteFolder} onDeleteFolder={handleDeleteFolder}
onUpdateFolder={handleUpdateFolder} onUpdateFolder={handleUpdateFolder}
/> />
<IconArrowBarRight <button
className="fixed top-5 right-[270px] z-50 h-7 w-7 cursor-pointer hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-[270px] sm:h-8 sm:w-8 sm:text-neutral-700" className="fixed top-5 right-[270px] z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-[270px] sm:h-8 sm:w-8 sm:text-neutral-700"
onClick={handleTogglePromptbar} onClick={handleTogglePromptbar}
/> >
<IconArrowBarRight />
</button>
<div <div
onClick={handleTogglePromptbar} onClick={handleTogglePromptbar}
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden" className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
></div> ></div>
</div> </div>
) : ( ) : (
<IconArrowBarLeft <button
className="fixed top-2.5 right-4 z-50 h-7 w-7 cursor-pointer text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-4 sm:h-8 sm:w-8 sm:text-neutral-700" className="fixed top-2.5 right-4 z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-4 sm:h-8 sm:w-8 sm:text-neutral-700"
onClick={handleTogglePromptbar} onClick={handleTogglePromptbar}
/> >
<IconArrowBarLeft />
</button>
)} )}
</div> </div>
</main> </main>

View File

@ -9,5 +9,10 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
variants: {
extend: {
visibility: ["group-hover"],
},
},
plugins: [require('@tailwindcss/typography')], plugins: [require('@tailwindcss/typography')],
}; };