Skip to content
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
1 change: 1 addition & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
## Accessibility

- **Config form labels now focus their field when clicked and read correctly to screen readers.** Many settings/config forms (AI Providers, DataDog, feature-agent config, message & calendar account setup, scheduled-task provider/model pickers, the agent world/schedule tabs, MeatSpace nicotine + POST drills, and more) rendered the label as a plain sibling of its input with no association, so clicking the label did nothing and assistive tech couldn't announce the pairing. These fields now flow through a shared `FormField` wrapper that generates a stable id and wires `htmlFor`/`id` automatically, keeping the exact same styling (#2027). Remaining forms are tracked for a follow-up sweep (#2051).
- **The follow-up sweep wired the remaining config-form labels to their controls.** Continuing #2027, ~130 more sibling `<label>`+input pairs across ~40 files — agents (overview/list/published), digital-twin tabs, image-gen controls, media/prompt tools, MeatSpace panels, brain, CoS memory/resume, writers-room, and pages (PromptManager, VideoGen, Jira, Sharing, Loras, Security, ImageGen, Templates, Loops, Browser, and more) — now flow through the shared `FormField` wrapper so clicking a label focuses its field and screen readers announce the pairing. Accessibility only: existing Tailwind classes are preserved verbatim, wrapping `<label><input/></label>` and radio/toggle group labels are left untouched, and controls whose custom component doesn't forward `id` to a DOM input were deliberately skipped (#2051).

## Layout

Expand Down
46 changes: 19 additions & 27 deletions client/src/components/agents/AgentList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { Sparkles } from 'lucide-react';
import toast from '../ui/Toast';
import BrailleSpinner from '../BrailleSpinner';
import { FormField } from '../ui/FormField';
import * as api from '../../services/api';
import { PERSONALITY_STYLES, DEFAULT_PERSONALITY, DEFAULT_AVATAR } from './constants';
import { filterSelectableModels } from '../../utils/providers';
Expand Down Expand Up @@ -216,8 +217,7 @@ export default function AgentList() {
<span className="text-sm font-medium text-white">Generate with AI</span>
</div>
<div className="flex items-end gap-3">
<div className="flex-1">
<label className="block text-xs text-gray-400 mb-1">Provider</label>
<FormField label="Provider" className="flex-1" labelClassName="block text-xs text-gray-400 mb-1">
<select
value={selectedProviderId}
onChange={(e) => { setSelectedProviderId(e.target.value); setSelectedModel(''); }}
Expand All @@ -229,9 +229,8 @@ export default function AgentList() {
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
<div className="flex-1">
<label className="block text-xs text-gray-400 mb-1">Model</label>
</FormField>
<FormField label="Model" className="flex-1" labelClassName="block text-xs text-gray-400 mb-1">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
Expand All @@ -243,7 +242,7 @@ export default function AgentList() {
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
</FormField>
<button
type="button"
onClick={handleGenerate}
Expand All @@ -263,18 +262,16 @@ export default function AgentList() {
</div>

<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Name</label>
<FormField label="Name">
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
required
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Style</label>
</FormField>
<FormField label="Style">
<select
value={formData.personality.style}
onChange={(e) => updatePersonality('style', e.target.value)}
Expand All @@ -284,32 +281,29 @@ export default function AgentList() {
<option key={style.value} value={style.value}>{style.label}</option>
))}
</select>
</div>
</FormField>
</div>

<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Description</label>
<FormField label="Description" className="mb-4">
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white h-20"
placeholder="Brief description of this agent's purpose..."
/>
</div>
</FormField>

<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Tone</label>
<FormField label="Tone" className="mb-4">
<input
type="text"
value={formData.personality.tone}
onChange={(e) => updatePersonality('tone', e.target.value)}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., friendly but informative"
/>
</div>
</FormField>

<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Topics (comma-separated)</label>
<FormField label="Topics (comma-separated)" className="mb-4">
<input
type="text"
value={topicsText}
Expand All @@ -318,10 +312,9 @@ export default function AgentList() {
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., technology, AI, philosophy"
/>
</div>
</FormField>

<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Quirks (comma-separated)</label>
<FormField label="Quirks (comma-separated)" className="mb-4">
<input
type="text"
value={quirksText}
Expand All @@ -330,17 +323,16 @@ export default function AgentList() {
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., uses metaphors, asks follow-up questions"
/>
</div>
</FormField>

<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Prompt Prefix</label>
<FormField label="Prompt Prefix" className="mb-4">
<textarea
value={formData.personality.promptPrefix}
onChange={(e) => updatePersonality('promptPrefix', e.target.value)}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white h-24 font-mono text-sm"
placeholder="Custom instructions injected into AI prompts..."
/>
</div>
</FormField>

<div className="flex items-center gap-4 mb-4">
<label className="block text-sm text-gray-400">Avatar Emoji</label>
Expand Down
66 changes: 27 additions & 39 deletions client/src/components/agents/tabs/OverviewTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import toast from '../../ui/Toast';
import * as api from '../../../services/api';
import { filterSelectableModels } from '../../../utils/providers';
import BrailleSpinner from '../../BrailleSpinner';
import { FormField } from '../../ui/FormField';
import { PERSONALITY_STYLES, DEFAULT_PERSONALITY, DEFAULT_AVATAR, PLATFORM_TYPES, ACCOUNT_STATUSES } from '../constants';
import { useCooldownTick } from '../../../hooks/useCooldownTick';
import { formatCooldown, formatDateTime } from '../../../utils/formatters';
Expand Down Expand Up @@ -309,18 +310,16 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
<form onSubmit={handleSave}>
{/* Name + Style */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Name</label>
<FormField label="Name">
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
required
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Style</label>
</FormField>
<FormField label="Style">
<select
value={formData.personality.style}
onChange={(e) => updatePersonality('style', e.target.value)}
Expand All @@ -330,57 +329,52 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
<option key={style.value} value={style.value}>{style.label}</option>
))}
</select>
</div>
</FormField>
</div>

{/* Description */}
<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Description</label>
<FormField label="Description" className="mb-4">
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white h-20"
placeholder="Brief description of this agent's purpose..."
/>
</div>
</FormField>

{/* Tone + Avatar (emoji + color) */}
<div className="flex items-end gap-4 mb-4">
<div className="flex-1">
<label className="block text-sm text-gray-400 mb-1">Tone</label>
<FormField label="Tone" className="flex-1">
<input
type="text"
value={formData.personality.tone}
onChange={(e) => updatePersonality('tone', e.target.value)}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., friendly but informative"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-400">Emoji</label>
</FormField>
<FormField label="Emoji" className="flex items-center gap-2" labelClassName="text-sm text-gray-400">
<input
type="text"
value={formData.avatar.emoji || ''}
onChange={(e) => setFormData({ ...formData, avatar: { ...formData.avatar, emoji: e.target.value } })}
className="w-14 px-2 py-2 bg-port-bg border border-port-border rounded text-white text-center"
maxLength={2}
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-400">Color</label>
</FormField>
<FormField label="Color" className="flex items-center gap-2" labelClassName="text-sm text-gray-400">
<input
type="color"
value={formData.avatar.color || '#3b82f6'}
onChange={(e) => setFormData({ ...formData, avatar: { ...formData.avatar, color: e.target.value } })}
className="w-12 h-9 border border-port-border rounded cursor-pointer"
/>
</div>
</FormField>
</div>

{/* Topics + Quirks */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Topics (comma-separated)</label>
<FormField label="Topics (comma-separated)">
<input
type="text"
value={topicsText}
Expand All @@ -389,9 +383,8 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., technology, AI"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Quirks (comma-separated)</label>
</FormField>
<FormField label="Quirks (comma-separated)">
<input
type="text"
value={quirksText}
Expand All @@ -400,19 +393,18 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white"
placeholder="e.g., uses metaphors"
/>
</div>
</FormField>
</div>

{/* Prompt Prefix */}
<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Prompt Prefix</label>
<FormField label="Prompt Prefix" className="mb-4">
<textarea
value={formData.personality.promptPrefix}
onChange={(e) => updatePersonality('promptPrefix', e.target.value)}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded text-white h-24 font-mono text-sm"
placeholder="Custom instructions injected into AI prompts..."
/>
</div>
</FormField>

<button
type="submit"
Expand All @@ -435,8 +427,7 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
) : (
<>
{activeAccounts.length > 1 && (
<div className="mb-3">
<label className="block text-xs text-gray-400 mb-1">Account</label>
<FormField label="Account" className="mb-3" labelClassName="block text-xs text-gray-400 mb-1">
<select
value={quickAccountId}
onChange={(e) => setQuickAccountId(e.target.value)}
Expand All @@ -446,7 +437,7 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
<option key={a.id} value={a.id}>{a.credentials.username}</option>
))}
</select>
</div>
</FormField>
)}

{rateLimits && (
Expand Down Expand Up @@ -614,8 +605,7 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
{showAccountForm && (
<form onSubmit={handleAccountSubmit} className="mb-4 p-3 bg-port-bg border border-port-border rounded-lg">
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<label className="block text-sm text-gray-400 mb-1">Platform</label>
<FormField label="Platform">
<select
value={accountForm.platform}
onChange={(e) => setAccountForm({ ...accountForm, platform: e.target.value })}
Expand All @@ -627,9 +617,8 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Account Name</label>
</FormField>
<FormField label="Account Name">
<input
type="text"
value={accountForm.name}
Expand All @@ -638,17 +627,16 @@ export default function OverviewTab({ agentId, agent, onAgentUpdate }) {
placeholder="Display name on platform"
required
/>
</div>
</FormField>
</div>
<div className="mb-3">
<label className="block text-sm text-gray-400 mb-1">Bio/Description</label>
<FormField label="Bio/Description" className="mb-3">
<textarea
value={accountForm.description}
onChange={(e) => setAccountForm({ ...accountForm, description: e.target.value })}
className="w-full px-3 py-2 bg-port-card border border-port-border rounded text-white h-16"
placeholder="Brief bio for the platform profile..."
/>
</div>
</FormField>
<div className="flex gap-2">
<button type="submit" className="px-3 py-1.5 text-sm bg-port-success text-white rounded hover:bg-port-success/80">
Register Account
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/agents/tabs/PublishedTab.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import * as api from '../../../services/api';
import BrailleSpinner from '../../BrailleSpinner';
import { FormField } from '../../ui/FormField';
import { timeAgo, formatDateTime } from '../../../utils/formatters';

export default function PublishedTab({ agentId }) {
Expand Down Expand Up @@ -49,8 +50,7 @@ export default function PublishedTab({ agentId }) {
<div className="p-4">
{/* Header */}
<div className="flex flex-wrap items-end gap-4 mb-6 p-4 bg-port-card border border-port-border rounded-lg">
<div>
<label className="block text-sm text-gray-400 mb-1">Account</label>
<FormField label="Account">
<select
value={selectedAccountId}
onChange={(e) => setSelectedAccountId(e.target.value)}
Expand All @@ -63,7 +63,7 @@ export default function PublishedTab({ agentId }) {
</option>
))}
</select>
</div>
</FormField>
<div className="flex items-center gap-2 ml-auto">
<select
value={publishedDays}
Expand Down
Loading