Skip to content

webui : improve accessibility for visually impaired people #13551

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

Merged
merged 5 commits into from
May 16, 2025
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
Binary file modified tools/server/public/index.html.gz
Binary file not shown.
4 changes: 2 additions & 2 deletions tools/server/webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ function AppLayout() {
return (
<>
<Sidebar />
<div
<main
className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
id="main-scroll"
>
<Header />
<Outlet />
</div>
</main>
{
<SettingDialog
show={showSettings}
Expand Down
34 changes: 28 additions & 6 deletions tools/server/webui/src/components/ChatInputExtraContextItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@ export default function ChatInputExtraContextItem({
if (!items) return null;

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) => (
<div
className="indicator"
key={i}
onClick={() => clickToShow && setShow(i)}
tabIndex={0}
aria-description={
clickToShow ? `Click to show: ${item.name}` : undefined
}
role={clickToShow ? 'button' : 'menuitem'}
>
{removeItem && (
<div className="indicator-item indicator-top">
<button
aria-label="Remove file"
className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
onClick={() => removeItem(i)}
>
Expand All @@ -46,13 +56,16 @@ export default function ChatInputExtraContextItem({
<>
<img
src={item.base64Url}
alt={item.name}
alt={`Preview image for ${item.name}`}
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" />
</div>

Expand All @@ -66,16 +79,25 @@ export default function ChatInputExtraContextItem({
))}

{showingItem && (
<dialog className="modal modal-open">
<dialog
className="modal modal-open"
aria-description={`Preview ${showingItem.name}`}
>
<div className="modal-box">
<div className="flex justify-between items-center mb-4">
<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)} />
</button>
</div>
{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">
<pre className="whitespace-pre-wrap break-words text-sm">
Expand Down
42 changes: 29 additions & 13 deletions tools/server/webui/src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,20 @@ export default function ChatMessage({

if (!viewingChat) return null;

const isUser = msg.role === 'user';

return (
<div className="group" id={id}>
<div
className="group"
id={id}
role="group"
aria-description={`Message from ${msg.role}`}
>
<div
className={classNames({
chat: true,
'chat-start': msg.role !== 'user',
'chat-end': msg.role === 'user',
'chat-start': !isUser,
'chat-end': isUser,
})}
>
{msg.extra && msg.extra.length > 0 && (
Expand All @@ -99,7 +106,7 @@ export default function ChatMessage({
<div
className={classNames({
'chat-bubble markdown': true,
'chat-bubble bg-transparent': msg.role !== 'user',
'chat-bubble bg-transparent': !isUser,
})}
>
{/* textarea for editing message */}
Expand Down Expand Up @@ -142,7 +149,7 @@ export default function ChatMessage({
) : (
<>
{/* render message as markdown */}
<div dir="auto">
<div dir="auto" tabIndex={0}>
{thought && (
<ThoughtProcess
isThinking={!!isThinking && !!isPending}
Expand Down Expand Up @@ -196,13 +203,18 @@ export default function ChatMessage({
})}
>
{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
className={classNames({
'btn btn-sm btn-ghost p-1': true,
'opacity-20': !prevSibling,
})}
onClick={() => prevSibling && onChangeSibling(prevSibling)}
aria-label="Previous message version"
>
<ChevronLeftIcon className="h-4 w-4" />
</button>
Expand All @@ -215,6 +227,7 @@ export default function ChatMessage({
'opacity-20': !nextSibling,
})}
onClick={() => nextSibling && onChangeSibling(nextSibling)}
aria-label="Next message version"
>
<ChevronRightIcon className="h-4 w-4" />
</button>
Expand All @@ -223,7 +236,7 @@ export default function ChatMessage({
{/* user message */}
{msg.role === 'user' && (
<BtnWithTooltips
className="btn-mini show-on-hover w-8 h-8"
className="btn-mini w-8 h-8"
onClick={() => setEditingContent(msg.content)}
disabled={msg.content === null}
tooltipsContent="Edit message"
Expand All @@ -236,7 +249,7 @@ export default function ChatMessage({
<>
{!isPending && (
<BtnWithTooltips
className="btn-mini show-on-hover w-8 h-8"
className="btn-mini w-8 h-8"
onClick={() => {
if (msg.content !== null) {
onRegenerateMessage(msg as Message);
Expand All @@ -250,10 +263,7 @@ export default function ChatMessage({
)}
</>
)}
<CopyButton
className="btn-mini show-on-hover w-8 h-8"
content={msg.content}
/>
<CopyButton className="btn-mini w-8 h-8" content={msg.content} />
</div>
)}
</div>
Expand All @@ -271,6 +281,8 @@ function ThoughtProcess({
}) {
return (
<div
role="button"
aria-label="Toggle thought process display"
tabIndex={0}
className={classNames({
'collapse bg-none': true,
Expand All @@ -292,7 +304,11 @@ function ThoughtProcess({
)}
</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">
<MarkdownDisplay content={content} />
</div>
Expand Down
13 changes: 11 additions & 2 deletions tools/server/webui/src/components/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,11 @@ export default function ChatScreen() {
function ServerInfo() {
const { serverProps } = useAppContext();
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">
<b>Server Info</b>
<p>
Expand Down Expand Up @@ -311,6 +315,8 @@ function ChatInput({

return (
<div
role="group"
aria-label="Chat input"
className={classNames({
'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
Expand Down Expand Up @@ -400,13 +406,15 @@ function ChatInput({
'btn w-8 h-8 p-0 rounded-full': true,
'btn-disabled': isGenerating,
})}
aria-label="Upload file"
tabIndex={0}
role="button"
>
<PaperClipIcon className="h-5 w-5" />
</label>
<input
id="file-upload"
type="file"
className="hidden"
disabled={isGenerating}
{...getInputProps()}
hidden
Expand All @@ -422,6 +430,7 @@ function ChatInput({
<button
className="btn btn-primary w-8 h-8 p-0 rounded-full"
onClick={onSend}
aria-label="Send message"
>
<ArrowUpIcon className="h-5 w-5" />
</button>
Expand Down
8 changes: 6 additions & 2 deletions tools/server/webui/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ export default function Header() {

{/* action buttons (top right) */}
<div className="flex items-center">
<div className="tooltip tooltip-bottom" data-tip="Settings">
<button className="btn" onClick={() => setShowSettings(true)}>
<div
className="tooltip tooltip-bottom"
data-tip="Settings"
onClick={() => setShowSettings(true)}
>
<button className="btn" aria-hidden={true}>
{/* settings button */}
<Cog8ToothIcon className="w-5 h-5" />
</button>
Expand Down
22 changes: 17 additions & 5 deletions tools/server/webui/src/components/SettingDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,22 @@ export default function SettingDialog({
};

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">
<h3 className="text-lg font-bold mb-6">Settings</h3>
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
{/* 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) => (
<div
<button
key={idx}
className={classNames({
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
Expand All @@ -352,12 +360,16 @@ export default function SettingDialog({
dir="auto"
>
{section.title}
</div>
</button>
))}
</div>

{/* 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">
<summary className="btn bt-sm w-full m-1">
{SETTING_SECTIONS[sectionIdx].title}
Expand Down
Loading