-
-
Notifications
You must be signed in to change notification settings - Fork 6
🎨 Palette: Improve accessibility and loading feedback #525
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ## 2025-05-14 - [Icon-only Button Accessibility] | ||
| **Learning:** Many icon-only buttons in the application were missing ARIA labels and titles, making them inaccessible to screen readers and providing no tooltips for sighted users. The logo functioning as a history toggle was particularly non-obvious. | ||
| **Action:** Always provide both `aria-label` and `title` for icon-only buttons. Ensure that branding elements used as interactive controls have clear descriptive labels. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils' | |||||||||||||||||||||||||||||||||||||||||||
| import { UserMessage } from './user-message' | ||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from './ui/button' | ||||||||||||||||||||||||||||||||||||||||||||
| import { ArrowRight, Plus, Paperclip, X, Sprout } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||||||
| import { Spinner } from './ui/spinner' | ||||||||||||||||||||||||||||||||||||||||||||
| import Textarea from 'react-textarea-autosize' | ||||||||||||||||||||||||||||||||||||||||||||
| import { nanoid } from '@/lib/utils' | ||||||||||||||||||||||||||||||||||||||||||||
| import { useSettingsStore } from '@/lib/store/settings' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,19 +21,21 @@ interface ChatPanelProps { | |||||||||||||||||||||||||||||||||||||||||||
| input: string | ||||||||||||||||||||||||||||||||||||||||||||
| setInput: (value: string) => void | ||||||||||||||||||||||||||||||||||||||||||||
| onSuggestionsChange?: (suggestions: PartialRelated | null) => void | ||||||||||||||||||||||||||||||||||||||||||||
| onSubmitting?: (isSubmitting: boolean) => void | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface ChatPanelRef { | ||||||||||||||||||||||||||||||||||||||||||||
| handleAttachmentClick: () => void | ||||||||||||||||||||||||||||||||||||||||||||
| submitForm: () => void | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput, onSuggestionsChange }, ref) => { | ||||||||||||||||||||||||||||||||||||||||||||
| export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput, onSuggestionsChange, onSubmitting }, ref) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const [, setMessages] = useUIState<typeof AI>() | ||||||||||||||||||||||||||||||||||||||||||||
| const { submit, clearChat } = useActions() | ||||||||||||||||||||||||||||||||||||||||||||
| const { mapProvider } = useSettingsStore() | ||||||||||||||||||||||||||||||||||||||||||||
| const [isMobile, setIsMobile] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||
| const [selectedFile, setSelectedFile] = useState<File | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||
| const [isSubmitting, setIsSubmitting] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||
| const [suggestions, setSuggestionsState] = useState<PartialRelated | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||
| const setSuggestions = useCallback((s: PartialRelated | null) => { | ||||||||||||||||||||||||||||||||||||||||||||
| setSuggestionsState(s) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,8 +124,15 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||||||||||||
| setInput('') | ||||||||||||||||||||||||||||||||||||||||||||
| clearAttachment() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const responseMessage = await submit(formData) | ||||||||||||||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [...currentMessages, responseMessage as any]) | ||||||||||||||||||||||||||||||||||||||||||||
| setIsSubmitting(true) | ||||||||||||||||||||||||||||||||||||||||||||
| onSubmitting?.(true) | ||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
| const responseMessage = await submit(formData) | ||||||||||||||||||||||||||||||||||||||||||||
| setMessages(currentMessages => [...currentMessages, responseMessage as any]) | ||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||
| setIsSubmitting(false) | ||||||||||||||||||||||||||||||||||||||||||||
| onSubmitting?.(false) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+127
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling in submit flow — unhandled rejection if The 🐛 Proposed fix: add error handling setIsSubmitting(true)
onSubmitting?.(true)
try {
const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
+ } catch (error) {
+ console.error('Failed to submit message:', error)
+ // Consider showing a toast or inline error to the user
} finally {
setIsSubmitting(false)
onSubmitting?.(false)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleClear = async () => { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -177,6 +187,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||||||||||||
| onClick={() => handleClear()} | ||||||||||||||||||||||||||||||||||||||||||||
| data-testid="new-chat-button" | ||||||||||||||||||||||||||||||||||||||||||||
| title="New Chat" | ||||||||||||||||||||||||||||||||||||||||||||
| aria-label="New Chat" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| <Sprout size={28} className="fill-primary/20" /> | ||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -225,6 +236,8 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleAttachmentClick} | ||||||||||||||||||||||||||||||||||||||||||||
| data-testid="desktop-attachment-button" | ||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Attach File" | ||||||||||||||||||||||||||||||||||||||||||||
| title="Attach File" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| <Paperclip size={isMobile ? 18 : 20} /> | ||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -281,11 +294,12 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||||||||||||
| 'absolute top-1/2 transform -translate-y-1/2', | ||||||||||||||||||||||||||||||||||||||||||||
| isMobile ? 'right-1' : 'right-2' | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| disabled={input.length === 0 && !selectedFile} | ||||||||||||||||||||||||||||||||||||||||||||
| disabled={(input.length === 0 && !selectedFile) || isSubmitting} | ||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Send message" | ||||||||||||||||||||||||||||||||||||||||||||
| title="Send Message" | ||||||||||||||||||||||||||||||||||||||||||||
| data-testid="chat-submit" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| <ArrowRight size={isMobile ? 18 : 20} /> | ||||||||||||||||||||||||||||||||||||||||||||
| {isSubmitting ? <Spinner /> : <ArrowRight size={isMobile ? 18 : 20} />} | ||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -295,7 +309,14 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i | |||||||||||||||||||||||||||||||||||||||||||
| <span className="text-sm text-muted-foreground truncate max-w-xs"> | ||||||||||||||||||||||||||||||||||||||||||||
| {selectedFile.name} | ||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||
| <Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button"> | ||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||||||||||||||||||||||||
| size="icon" | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={clearAttachment} | ||||||||||||||||||||||||||||||||||||||||||||
| data-testid="clear-attachment-button" | ||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Remove Attachment" | ||||||||||||||||||||||||||||||||||||||||||||
| title="Remove Attachment" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| <X size={16} /> | ||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ export function Chat({ id }: ChatProps) { | |
| const [input, setInput] = useState('') | ||
| const [showEmptyScreen, setShowEmptyScreen] = useState(false) | ||
| const [isSubmitting, setIsSubmitting] = useState(false) | ||
| const [isSubmittingAction, setIsSubmittingAction] = useState(false) | ||
|
Comment on lines
40
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Confusing naming between
♻️ Proposed rename for clarity- const [isSubmitting, setIsSubmitting] = useState(false)
- const [isSubmittingAction, setIsSubmittingAction] = useState(false)
+ const [shouldTriggerSubmit, setShouldTriggerSubmit] = useState(false)
+ const [isSubmitting, setIsSubmitting] = useState(false)Then update all references accordingly: 🤖 Prompt for AI Agents
Comment on lines
38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are now two similarly-named submission states in This is a correctness risk because different UI elements may consult different flags (now or in future edits), leading to inconsistent submission UX. SuggestionConsolidate to a single submission flag in If Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion. |
||
| const [suggestions, setSuggestions] = useState<PartialRelated | null>(null) | ||
| const chatPanelRef = useRef<ChatPanelRef>(null); | ||
|
|
||
|
|
@@ -132,7 +133,11 @@ export function Chat({ id }: ChatProps) { | |
| {activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />} | ||
| </div> | ||
| <div className="mobile-icons-bar"> | ||
| <MobileIconsBar onAttachmentClick={handleAttachment} onSubmitClick={handleMobileSubmit} /> | ||
| <MobileIconsBar | ||
| onAttachmentClick={handleAttachment} | ||
| onSubmitClick={handleMobileSubmit} | ||
| isSubmitting={isSubmittingAction} | ||
| /> | ||
| </div> | ||
| <div className="mobile-chat-input-area"> | ||
| <ChatPanel | ||
|
|
@@ -141,6 +146,7 @@ export function Chat({ id }: ChatProps) { | |
| input={input} | ||
| setInput={setInput} | ||
| onSuggestionsChange={setSuggestions} | ||
| onSubmitting={setIsSubmittingAction} | ||
| /> | ||
| </div> | ||
| <div className="mobile-chat-messages-area relative"> | ||
|
|
@@ -185,6 +191,7 @@ export function Chat({ id }: ChatProps) { | |
| input={input} | ||
| setInput={setInput} | ||
| onSuggestionsChange={setSuggestions} | ||
| onSubmitting={setIsSubmittingAction} | ||
| /> | ||
| <div className="relative min-h-[100px]"> | ||
| <div className={cn("transition-all duration-300", suggestions ? "blur-md pointer-events-none" : "")}> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,13 +19,15 @@ import { MapToggle } from './map-toggle' | |
| import { ModeToggle } from './mode-toggle' | ||
| import { ProfileToggle } from './profile-toggle' | ||
| import { useCalendarToggle } from './calendar-toggle-context' | ||
| import { Spinner } from './ui/spinner' | ||
|
|
||
| interface MobileIconsBarProps { | ||
| onAttachmentClick: () => void; | ||
| onSubmitClick: () => void; | ||
| isSubmitting?: boolean; | ||
| } | ||
|
|
||
| export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClick, onSubmitClick }) => { | ||
| export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClick, onSubmitClick, isSubmitting }) => { | ||
| const [, setMessages] = useUIState<typeof AI>() | ||
| const { clearChat } = useActions() | ||
| const { toggleCalendar } = useCalendarToggle() | ||
|
|
@@ -37,27 +39,71 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic | |
|
|
||
| return ( | ||
| <div className="mobile-icons-bar-content"> | ||
| <Button variant="ghost" size="icon" onClick={handleNewChat} data-testid="mobile-new-chat-button"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| onClick={handleNewChat} | ||
| data-testid="mobile-new-chat-button" | ||
| aria-label="New Chat" | ||
| title="New Chat" | ||
| > | ||
| <Plus className="h-[1.2rem] w-[1.2rem]" /> | ||
| </Button> | ||
| <ProfileToggle /> | ||
| <MapToggle /> | ||
| <Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| onClick={toggleCalendar} | ||
| title="Open Calendar" | ||
| aria-label="Open Calendar" | ||
| data-testid="mobile-calendar-button" | ||
| > | ||
| <CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
| <Button variant="ghost" size="icon" data-testid="mobile-search-button"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| data-testid="mobile-search-button" | ||
| aria-label="Search" | ||
| title="Search" | ||
| > | ||
| <Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
|
Comment on lines
+64
to
72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Search button has no This button renders but does nothing when clicked. Was this intentional as a placeholder, or should it be wired to a search action? #!/bin/bash
# Check if the mobile search button had an onClick in previous versions or if there's a related handler
rg -n 'mobile-search-button' --type=tsx --type=ts -C3🤖 Prompt for AI Agents |
||
| <a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer"> | ||
| <Button variant="ghost" size="icon"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| aria-label="Purchase Credits" | ||
| title="Purchase Credits" | ||
| > | ||
| <TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
| </a> | ||
| <Button variant="ghost" size="icon" onClick={onAttachmentClick} data-testid="mobile-attachment-button"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| onClick={onAttachmentClick} | ||
| data-testid="mobile-attachment-button" | ||
| aria-label="Attach File" | ||
| title="Attach File" | ||
| > | ||
| <Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
| <Button variant="ghost" size="icon" data-testid="mobile-submit-button" onClick={onSubmitClick}> | ||
| <ArrowRight className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| data-testid="mobile-submit-button" | ||
| onClick={onSubmitClick} | ||
| aria-label="Send Message" | ||
| title="Send Message" | ||
| disabled={isSubmitting} | ||
| > | ||
| {isSubmitting ? ( | ||
| <Spinner /> | ||
| ) : ( | ||
| <ArrowRight className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| )} | ||
| </Button> | ||
| <History location="header" /> | ||
| <ModeToggle /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The response message is appended with
responseMessage as any. Even iftscpasses, this is an unsafe escape hatch that can hide real shape/compat issues at runtime and makes the state harder to reason about.Since this code path is central (chat submission), avoiding
anyhere is worth it.Suggestion
Avoid
as anyby tightening the return type ofsubmit(or by normalizing the message into the expected UI message type before pushing into state).If
submitreturns a union, consider a small runtime discriminator/normalizer sosetMessagesalways receives the same message shape.Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.