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:
parent
5d31947ab9
commit
a78a8c4a94
|
@ -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">
|
||||
{t('Model')}: {conversation.model.name}
|
||||
<IconSettings
|
||||
<button
|
||||
className="ml-2 cursor-pointer hover:opacity-50"
|
||||
onClick={handleSettings}
|
||||
size={18}
|
||||
/>
|
||||
<IconClearAll
|
||||
>
|
||||
<IconSettings size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 cursor-pointer hover:opacity-50"
|
||||
onClick={onClearAll}
|
||||
size={18}
|
||||
/>
|
||||
onClick={onClearAll}>
|
||||
<IconClearAll size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{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">
|
||||
|
|
|
@ -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">
|
||||
{messageIsStreaming && (
|
||||
<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}
|
||||
>
|
||||
<IconPlayerStop size={16} className="mb-[2px] inline-block" />{' '}
|
||||
{t('Stop Generating')}
|
||||
<IconPlayerStop size={16} /> {t('Stop Generating')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!messageIsStreaming && !conversationIsEmpty && (
|
||||
<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}
|
||||
>
|
||||
<IconRepeat size={16} className="mb-[2px] inline-block" />{' '}
|
||||
{t('Regenerate response')}
|
||||
<IconRepeat size={16} /> {t('Regenerate response')}
|
||||
</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
|
||||
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={{
|
||||
resize: 'none',
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
|
@ -279,12 +277,11 @@ export const ChatInput: FC<Props> = ({
|
|||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
<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}
|
||||
>
|
||||
<IconSend size={16} className="opacity-60" />
|
||||
<IconSend size={18} />
|
||||
</button>
|
||||
|
||||
{showPromptList && prompts.length > 0 && (
|
||||
|
|
|
@ -2,12 +2,12 @@ import { Message } from '@/types/chat';
|
|||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FC, memo, useEffect, useRef, useState } from 'react';
|
||||
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
||||
import rehypeMathjax from 'rehype-mathjax';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import { CodeBlock } from '../Markdown/CodeBlock';
|
||||
import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown';
|
||||
import { CopyButton } from './CopyButton';
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
|
@ -19,7 +19,6 @@ export const ChatMessage: FC<Props> = memo(
|
|||
({ message, messageIndex, onEditMessage }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||
const [messageContent, setMessageContent] = useState(message.content);
|
||||
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'
|
||||
}`}
|
||||
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="min-w-[40px] font-bold">
|
||||
<div className="min-w-[40px] font-bold text-right">
|
||||
{message.role === 'assistant' ? t('AI') : t('You')}:
|
||||
</div>
|
||||
|
||||
|
@ -94,7 +91,7 @@ export const ChatMessage: FC<Props> = memo(
|
|||
<div className="flex w-full flex-col">
|
||||
<textarea
|
||||
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}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handlePressEnter}
|
||||
|
@ -133,24 +130,44 @@ export const ChatMessage: FC<Props> = memo(
|
|||
</div>
|
||||
)}
|
||||
|
||||
{(isHovering || window.innerWidth < 640) && !isEditing && (
|
||||
{(window.innerWidth < 640 || !isEditing) && (
|
||||
<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
|
||||
? 'right-3 bottom-1'
|
||||
: 'right-0 top-[26px]'
|
||||
}`}
|
||||
>
|
||||
<IconEdit
|
||||
size={20}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
}
|
||||
`}
|
||||
onClick={toggleEditing}
|
||||
/>
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</button>
|
||||
)}
|
||||
</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
|
||||
className="prose dark:prose-invert"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
|
@ -197,13 +214,6 @@ export const ChatMessage: FC<Props> = memo(
|
|||
>
|
||||
{message.content}
|
||||
</MemoizedReactMarkdown>
|
||||
|
||||
{(isHovering || window.innerWidth < 640) && (
|
||||
<CopyButton
|
||||
messagedCopied={messagedCopied}
|
||||
copyOnClick={copyOnClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -17,7 +17,7 @@ export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => {
|
|||
</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">
|
||||
<select
|
||||
className="w-full bg-transparent p-2 outline-0"
|
||||
className="w-full bg-transparent p-2"
|
||||
placeholder={t('Select a model') || ''}
|
||||
value={model.id}
|
||||
onChange={(e) => {
|
||||
|
|
|
@ -19,13 +19,7 @@ export const PromptList: FC<Props> = ({
|
|||
return (
|
||||
<ul
|
||||
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)]"
|
||||
style={{
|
||||
width: 'calc(100% - 48px)',
|
||||
bottom: '100%',
|
||||
marginBottom: '4px',
|
||||
maxHeight: '200px',
|
||||
}}
|
||||
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"
|
||||
>
|
||||
{prompts.map((prompt, index) => (
|
||||
<li
|
||||
|
|
|
@ -14,10 +14,10 @@ export const Regenerate: FC<Props> = ({ onRegenerate }) => {
|
|||
{t('Sorry, there was an error.')}
|
||||
</div>
|
||||
<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}
|
||||
>
|
||||
<IconRefresh className="mr-2" />
|
||||
<IconRefresh />
|
||||
<div>{t('Regenerate response')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -196,7 +196,7 @@ export const SystemPrompt: FC<Props> = ({
|
|||
</label>
|
||||
<textarea
|
||||
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={{
|
||||
resize: 'none',
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
|
|
|
@ -194,11 +194,9 @@ export const Chatbar: FC<Props> = ({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 select-none text-center text-white opacity-50">
|
||||
<IconMessagesOff className="mx-auto mb-3" />
|
||||
<span className="text-[14px] leading-normal">
|
||||
<div className="flex flex-col gap-3 items-center text-sm leading-normal mt-8 text-white opacity-50">
|
||||
<IconMessagesOff />
|
||||
{t('No conversations.')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -64,18 +64,18 @@ export const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
|||
|
||||
<div className="flex items-center">
|
||||
<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}
|
||||
>
|
||||
{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')}
|
||||
</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}
|
||||
>
|
||||
<IconDownload size={18} />
|
||||
|
|
|
@ -655,21 +655,24 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
|||
onImportConversations={handleImportConversations}
|
||||
/>
|
||||
|
||||
<IconArrowBarLeft
|
||||
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"
|
||||
<button
|
||||
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}
|
||||
/>
|
||||
|
||||
>
|
||||
<IconArrowBarLeft />
|
||||
</button>
|
||||
<div
|
||||
onClick={handleToggleChatbar}
|
||||
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
|
||||
></div>
|
||||
</div>
|
||||
) : (
|
||||
<IconArrowBarRight
|
||||
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"
|
||||
<button
|
||||
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}
|
||||
/>
|
||||
>
|
||||
<IconArrowBarRight />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1">
|
||||
|
@ -702,20 +705,24 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
|||
onDeleteFolder={handleDeleteFolder}
|
||||
onUpdateFolder={handleUpdateFolder}
|
||||
/>
|
||||
<IconArrowBarRight
|
||||
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"
|
||||
<button
|
||||
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}
|
||||
/>
|
||||
>
|
||||
<IconArrowBarRight />
|
||||
</button>
|
||||
<div
|
||||
onClick={handleTogglePromptbar}
|
||||
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
|
||||
></div>
|
||||
</div>
|
||||
) : (
|
||||
<IconArrowBarLeft
|
||||
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"
|
||||
<button
|
||||
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}
|
||||
/>
|
||||
>
|
||||
<IconArrowBarLeft />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
@ -9,5 +9,10 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
visibility: ["group-hover"],
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue