mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-06-27 12:05:03 +00:00
webui : improve accessibility for visually impaired people (#13551)
* webui : improve accessibility for visually impaired people * add a11y for extra contents * fix some labels being read twice * add skip to main content
This commit is contained in:
Binary file not shown.
@ -28,13 +28,13 @@ function AppLayout() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div
|
<main
|
||||||
className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
|
className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
|
||||||
id="main-scroll"
|
id="main-scroll"
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</main>
|
||||||
{
|
{
|
||||||
<SettingDialog
|
<SettingDialog
|
||||||
show={showSettings}
|
show={showSettings}
|
||||||
|
@ -18,16 +18,26 @@ export default function ChatInputExtraContextItem({
|
|||||||
if (!items) return null;
|
if (!items) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1">
|
<div
|
||||||
|
className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1"
|
||||||
|
role="group"
|
||||||
|
aria-description="Selected files"
|
||||||
|
>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div
|
<div
|
||||||
className="indicator"
|
className="indicator"
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => clickToShow && setShow(i)}
|
onClick={() => clickToShow && setShow(i)}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-description={
|
||||||
|
clickToShow ? `Click to show: ${item.name}` : undefined
|
||||||
|
}
|
||||||
|
role={clickToShow ? 'button' : 'menuitem'}
|
||||||
>
|
>
|
||||||
{removeItem && (
|
{removeItem && (
|
||||||
<div className="indicator-item indicator-top">
|
<div className="indicator-item indicator-top">
|
||||||
<button
|
<button
|
||||||
|
aria-label="Remove file"
|
||||||
className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
|
className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
|
||||||
onClick={() => removeItem(i)}
|
onClick={() => removeItem(i)}
|
||||||
>
|
>
|
||||||
@ -46,13 +56,16 @@ export default function ChatInputExtraContextItem({
|
|||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
src={item.base64Url}
|
src={item.base64Url}
|
||||||
alt={item.name}
|
alt={`Preview image for ${item.name}`}
|
||||||
className="w-14 h-14 object-cover rounded-md"
|
className="w-14 h-14 object-cover rounded-md"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="w-14 h-14 flex items-center justify-center">
|
<div
|
||||||
|
className="w-14 h-14 flex items-center justify-center"
|
||||||
|
aria-description="Document icon"
|
||||||
|
>
|
||||||
<DocumentTextIcon className="h-8 w-14 text-base-content/50" />
|
<DocumentTextIcon className="h-8 w-14 text-base-content/50" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -66,16 +79,25 @@ export default function ChatInputExtraContextItem({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{showingItem && (
|
{showingItem && (
|
||||||
<dialog className="modal modal-open">
|
<dialog
|
||||||
|
className="modal modal-open"
|
||||||
|
aria-description={`Preview ${showingItem.name}`}
|
||||||
|
>
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<b>{showingItem.name ?? 'Extra content'}</b>
|
<b>{showingItem.name ?? 'Extra content'}</b>
|
||||||
<button className="btn btn-ghost btn-sm">
|
<button
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
aria-label="Close preview dialog"
|
||||||
|
>
|
||||||
<XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
|
<XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showingItem.type === 'imageFile' ? (
|
{showingItem.type === 'imageFile' ? (
|
||||||
<img src={showingItem.base64Url} alt={showingItem.name} />
|
<img
|
||||||
|
src={showingItem.base64Url}
|
||||||
|
alt={`Preview image for ${showingItem.name}`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<pre className="whitespace-pre-wrap break-words text-sm">
|
<pre className="whitespace-pre-wrap break-words text-sm">
|
||||||
|
@ -83,13 +83,20 @@ export default function ChatMessage({
|
|||||||
|
|
||||||
if (!viewingChat) return null;
|
if (!viewingChat) return null;
|
||||||
|
|
||||||
|
const isUser = msg.role === 'user';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group" id={id}>
|
<div
|
||||||
|
className="group"
|
||||||
|
id={id}
|
||||||
|
role="group"
|
||||||
|
aria-description={`Message from ${msg.role}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
chat: true,
|
chat: true,
|
||||||
'chat-start': msg.role !== 'user',
|
'chat-start': !isUser,
|
||||||
'chat-end': msg.role === 'user',
|
'chat-end': isUser,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{msg.extra && msg.extra.length > 0 && (
|
{msg.extra && msg.extra.length > 0 && (
|
||||||
@ -99,7 +106,7 @@ export default function ChatMessage({
|
|||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'chat-bubble markdown': true,
|
'chat-bubble markdown': true,
|
||||||
'chat-bubble bg-transparent': msg.role !== 'user',
|
'chat-bubble bg-transparent': !isUser,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* textarea for editing message */}
|
{/* textarea for editing message */}
|
||||||
@ -142,7 +149,7 @@ export default function ChatMessage({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* render message as markdown */}
|
{/* render message as markdown */}
|
||||||
<div dir="auto">
|
<div dir="auto" tabIndex={0}>
|
||||||
{thought && (
|
{thought && (
|
||||||
<ThoughtProcess
|
<ThoughtProcess
|
||||||
isThinking={!!isThinking && !!isPending}
|
isThinking={!!isThinking && !!isPending}
|
||||||
@ -196,13 +203,18 @@ export default function ChatMessage({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{siblingLeafNodeIds && siblingLeafNodeIds.length > 1 && (
|
{siblingLeafNodeIds && siblingLeafNodeIds.length > 1 && (
|
||||||
<div className="flex gap-1 items-center opacity-60 text-sm">
|
<div
|
||||||
|
className="flex gap-1 items-center opacity-60 text-sm"
|
||||||
|
role="navigation"
|
||||||
|
aria-description={`Message version ${siblingCurrIdx + 1} of ${siblingLeafNodeIds.length}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'btn btn-sm btn-ghost p-1': true,
|
'btn btn-sm btn-ghost p-1': true,
|
||||||
'opacity-20': !prevSibling,
|
'opacity-20': !prevSibling,
|
||||||
})}
|
})}
|
||||||
onClick={() => prevSibling && onChangeSibling(prevSibling)}
|
onClick={() => prevSibling && onChangeSibling(prevSibling)}
|
||||||
|
aria-label="Previous message version"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="h-4 w-4" />
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -215,6 +227,7 @@ export default function ChatMessage({
|
|||||||
'opacity-20': !nextSibling,
|
'opacity-20': !nextSibling,
|
||||||
})}
|
})}
|
||||||
onClick={() => nextSibling && onChangeSibling(nextSibling)}
|
onClick={() => nextSibling && onChangeSibling(nextSibling)}
|
||||||
|
aria-label="Next message version"
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -223,7 +236,7 @@ export default function ChatMessage({
|
|||||||
{/* user message */}
|
{/* user message */}
|
||||||
{msg.role === 'user' && (
|
{msg.role === 'user' && (
|
||||||
<BtnWithTooltips
|
<BtnWithTooltips
|
||||||
className="btn-mini show-on-hover w-8 h-8"
|
className="btn-mini w-8 h-8"
|
||||||
onClick={() => setEditingContent(msg.content)}
|
onClick={() => setEditingContent(msg.content)}
|
||||||
disabled={msg.content === null}
|
disabled={msg.content === null}
|
||||||
tooltipsContent="Edit message"
|
tooltipsContent="Edit message"
|
||||||
@ -236,7 +249,7 @@ export default function ChatMessage({
|
|||||||
<>
|
<>
|
||||||
{!isPending && (
|
{!isPending && (
|
||||||
<BtnWithTooltips
|
<BtnWithTooltips
|
||||||
className="btn-mini show-on-hover w-8 h-8"
|
className="btn-mini w-8 h-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (msg.content !== null) {
|
if (msg.content !== null) {
|
||||||
onRegenerateMessage(msg as Message);
|
onRegenerateMessage(msg as Message);
|
||||||
@ -250,10 +263,7 @@ export default function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CopyButton
|
<CopyButton className="btn-mini w-8 h-8" content={msg.content} />
|
||||||
className="btn-mini show-on-hover w-8 h-8"
|
|
||||||
content={msg.content}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -271,6 +281,8 @@ function ThoughtProcess({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
aria-label="Toggle thought process display"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'collapse bg-none': true,
|
'collapse bg-none': true,
|
||||||
@ -292,7 +304,11 @@ function ThoughtProcess({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="collapse-content text-base-content/70 text-sm p-1">
|
<div
|
||||||
|
className="collapse-content text-base-content/70 text-sm p-1"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-description="Thought process content"
|
||||||
|
>
|
||||||
<div className="border-l-2 border-base-content/20 pl-4 mb-4">
|
<div className="border-l-2 border-base-content/20 pl-4 mb-4">
|
||||||
<MarkdownDisplay content={content} />
|
<MarkdownDisplay content={content} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -279,7 +279,11 @@ export default function ChatScreen() {
|
|||||||
function ServerInfo() {
|
function ServerInfo() {
|
||||||
const { serverProps } = useAppContext();
|
const { serverProps } = useAppContext();
|
||||||
return (
|
return (
|
||||||
<div className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6">
|
<div
|
||||||
|
className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-description="Server information"
|
||||||
|
>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<b>Server Info</b>
|
<b>Server Info</b>
|
||||||
<p>
|
<p>
|
||||||
@ -311,6 +315,8 @@ function ChatInput({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-label="Chat input"
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true,
|
'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true,
|
||||||
'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
|
'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
|
||||||
@ -400,13 +406,15 @@ function ChatInput({
|
|||||||
'btn w-8 h-8 p-0 rounded-full': true,
|
'btn w-8 h-8 p-0 rounded-full': true,
|
||||||
'btn-disabled': isGenerating,
|
'btn-disabled': isGenerating,
|
||||||
})}
|
})}
|
||||||
|
aria-label="Upload file"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
>
|
>
|
||||||
<PaperClipIcon className="h-5 w-5" />
|
<PaperClipIcon className="h-5 w-5" />
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
type="file"
|
type="file"
|
||||||
className="hidden"
|
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
{...getInputProps()}
|
{...getInputProps()}
|
||||||
hidden
|
hidden
|
||||||
@ -422,6 +430,7 @@ function ChatInput({
|
|||||||
<button
|
<button
|
||||||
className="btn btn-primary w-8 h-8 p-0 rounded-full"
|
className="btn btn-primary w-8 h-8 p-0 rounded-full"
|
||||||
onClick={onSend}
|
onClick={onSend}
|
||||||
|
aria-label="Send message"
|
||||||
>
|
>
|
||||||
<ArrowUpIcon className="h-5 w-5" />
|
<ArrowUpIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -38,8 +38,12 @@ export default function Header() {
|
|||||||
|
|
||||||
{/* action buttons (top right) */}
|
{/* action buttons (top right) */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="tooltip tooltip-bottom" data-tip="Settings">
|
<div
|
||||||
<button className="btn" onClick={() => setShowSettings(true)}>
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip="Settings"
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
>
|
||||||
|
<button className="btn" aria-hidden={true}>
|
||||||
{/* settings button */}
|
{/* settings button */}
|
||||||
<Cog8ToothIcon className="w-5 h-5" />
|
<Cog8ToothIcon className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -335,14 +335,22 @@ export default function SettingDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog className={classNames({ modal: true, 'modal-open': show })}>
|
<dialog
|
||||||
|
className={classNames({ modal: true, 'modal-open': show })}
|
||||||
|
aria-label="Settings dialog"
|
||||||
|
>
|
||||||
<div className="modal-box w-11/12 max-w-3xl">
|
<div className="modal-box w-11/12 max-w-3xl">
|
||||||
<h3 className="text-lg font-bold mb-6">Settings</h3>
|
<h3 className="text-lg font-bold mb-6">Settings</h3>
|
||||||
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
|
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
|
||||||
{/* Left panel, showing sections - Desktop version */}
|
{/* Left panel, showing sections - Desktop version */}
|
||||||
<div className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200">
|
<div
|
||||||
|
className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200"
|
||||||
|
role="complementary"
|
||||||
|
aria-description="Settings sections"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
{SETTING_SECTIONS.map((section, idx) => (
|
{SETTING_SECTIONS.map((section, idx) => (
|
||||||
<div
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
|
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
|
||||||
@ -352,12 +360,16 @@ export default function SettingDialog({
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{section.title}
|
{section.title}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Left panel, showing sections - Mobile version */}
|
{/* Left panel, showing sections - Mobile version */}
|
||||||
<div className="md:hidden flex flex-row gap-2 mb-4">
|
{/* This menu is skipped on a11y, otherwise it's repeated the desktop version */}
|
||||||
|
<div
|
||||||
|
className="md:hidden flex flex-row gap-2 mb-4"
|
||||||
|
aria-disabled={true}
|
||||||
|
>
|
||||||
<details className="dropdown">
|
<details className="dropdown">
|
||||||
<summary className="btn bt-sm w-full m-1">
|
<summary className="btn bt-sm w-full m-1">
|
||||||
{SETTING_SECTIONS[sectionIdx].title}
|
{SETTING_SECTIONS[sectionIdx].title}
|
||||||
|
@ -50,44 +50,72 @@ export default function Sidebar() {
|
|||||||
id="toggle-drawer"
|
id="toggle-drawer"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="drawer-toggle"
|
className="drawer-toggle"
|
||||||
|
aria-label="Toggle sidebar"
|
||||||
defaultChecked
|
defaultChecked
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="drawer-side h-screen lg:h-screen z-50 lg:max-w-64">
|
<div
|
||||||
|
className="drawer-side h-screen lg:h-screen z-50 lg:max-w-64"
|
||||||
|
role="complementary"
|
||||||
|
aria-label="Sidebar"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
htmlFor="toggle-drawer"
|
htmlFor="toggle-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="Close sidebar"
|
||||||
className="drawer-overlay"
|
className="drawer-overlay"
|
||||||
></label>
|
></label>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#main-scroll"
|
||||||
|
className="absolute -left-80 top-0 w-1 h-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
|
||||||
<div className="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
|
<div className="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
|
||||||
<div className="flex flex-row items-center justify-between mb-4 mt-4">
|
<div className="flex flex-row items-center justify-between mb-4 mt-4">
|
||||||
<h2 className="font-bold ml-4">Conversations</h2>
|
<h2 className="font-bold ml-4" role="heading">
|
||||||
|
Conversations
|
||||||
|
</h2>
|
||||||
|
|
||||||
{/* close sidebar button */}
|
{/* close sidebar button */}
|
||||||
<label htmlFor="toggle-drawer" className="btn btn-ghost lg:hidden">
|
<label
|
||||||
|
htmlFor="toggle-drawer"
|
||||||
|
className="btn btn-ghost lg:hidden"
|
||||||
|
aria-label="Close sidebar"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<XMarkIcon className="w-5 h-5" />
|
<XMarkIcon className="w-5 h-5" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* new conversation button */}
|
{/* new conversation button */}
|
||||||
<div
|
<button
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'btn btn-ghost justify-start px-2': true,
|
'btn btn-ghost justify-start px-2': true,
|
||||||
'btn-soft': !currConv,
|
'btn-soft': !currConv,
|
||||||
})}
|
})}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
|
aria-label="New conversation"
|
||||||
>
|
>
|
||||||
<PencilSquareIcon className="w-5 h-5" />
|
<PencilSquareIcon className="w-5 h-5" />
|
||||||
New conversation
|
New conversation
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{/* list of conversations */}
|
{/* list of conversations */}
|
||||||
{groupedConv.map((group, i) => (
|
{groupedConv.map((group, i) => (
|
||||||
<div key={i}>
|
<div key={i} role="group">
|
||||||
{/* group name (by date) */}
|
{/* group name (by date) */}
|
||||||
{group.title ? (
|
{group.title ? (
|
||||||
// we use btn class here to make sure that the padding/margin are aligned with the other items
|
// we use btn class here to make sure that the padding/margin are aligned with the other items
|
||||||
<b className="btn btn-ghost btn-xs bg-none btn-disabled block text-xs text-base-content text-start px-2 mb-0 mt-6 font-bold">
|
<b
|
||||||
|
className="btn btn-ghost btn-xs bg-none btn-disabled block text-xs text-base-content text-start px-2 mb-0 mt-6 font-bold"
|
||||||
|
role="note"
|
||||||
|
aria-description={group.title}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
{group.title}
|
{group.title}
|
||||||
</b>
|
</b>
|
||||||
) : (
|
) : (
|
||||||
@ -184,20 +212,23 @@ function ConversationItem({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={conv.name}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'group flex flex-row btn btn-ghost justify-start items-center font-normal px-2 h-9':
|
'group flex flex-row btn btn-ghost justify-start items-center font-normal px-2 h-9':
|
||||||
true,
|
true,
|
||||||
'btn-soft': isCurrConv,
|
'btn-soft': isCurrConv,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
key={conv.id}
|
key={conv.id}
|
||||||
className="w-full overflow-hidden truncate text-start"
|
className="w-full overflow-hidden truncate text-start"
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{conv.name}
|
{conv.name}
|
||||||
</div>
|
</button>
|
||||||
<div className="dropdown dropdown-end h-5">
|
<div className="dropdown dropdown-end h-5">
|
||||||
<BtnWithTooltips
|
<BtnWithTooltips
|
||||||
// on mobile, we always show the ellipsis icon
|
// on mobile, we always show the ellipsis icon
|
||||||
@ -211,22 +242,23 @@ function ConversationItem({
|
|||||||
</BtnWithTooltips>
|
</BtnWithTooltips>
|
||||||
{/* dropdown menu */}
|
{/* dropdown menu */}
|
||||||
<ul
|
<ul
|
||||||
|
aria-label="More options"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="dropdown-content menu bg-base-100 rounded-box z-[1] p-2 shadow"
|
className="dropdown-content menu bg-base-100 rounded-box z-[1] p-2 shadow"
|
||||||
>
|
>
|
||||||
<li onClick={onRename}>
|
<li onClick={onRename} tabIndex={0}>
|
||||||
<a>
|
<a>
|
||||||
<PencilIcon className="w-4 h-4" />
|
<PencilIcon className="w-4 h-4" />
|
||||||
Rename
|
Rename
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li onClick={onDownload}>
|
<li onClick={onDownload} tabIndex={0}>
|
||||||
<a>
|
<a>
|
||||||
<ArrowDownTrayIcon className="w-4 h-4" />
|
<ArrowDownTrayIcon className="w-4 h-4" />
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="text-error" onClick={onDelete}>
|
<li className="text-error" onClick={onDelete} tabIndex={0}>
|
||||||
<a>
|
<a>
|
||||||
<TrashIcon className="w-4 h-4" />
|
<TrashIcon className="w-4 h-4" />
|
||||||
Delete
|
Delete
|
||||||
|
@ -34,9 +34,6 @@ html {
|
|||||||
/* TODO: fix markdown table */
|
/* TODO: fix markdown table */
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-on-hover {
|
|
||||||
@apply md:opacity-0 md:group-hover:opacity-100;
|
|
||||||
}
|
|
||||||
.btn-mini {
|
.btn-mini {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
}
|
}
|
||||||
|
@ -52,13 +52,20 @@ export function BtnWithTooltips({
|
|||||||
tooltipsContent: string;
|
tooltipsContent: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
// the onClick handler is on the container, so screen readers can safely ignore the inner button
|
||||||
|
// this prevents the label from being read twice
|
||||||
return (
|
return (
|
||||||
<div className="tooltip tooltip-bottom" data-tip={tooltipsContent}>
|
<div
|
||||||
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip={tooltipsContent}
|
||||||
|
role="button"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className={`${className ?? ''} flex items-center justify-center`}
|
className={`${className ?? ''} flex items-center justify-center`}
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
aria-hidden={true}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
Reference in New Issue
Block a user