a11y_agent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.sample +3 -0
- data/README.md +20 -0
- data/eslint.config.mjs +23 -0
- data/fixtures/sample.tsx +100 -0
- data/lib/a11y_agent/version.rb +5 -0
- data/lib/a11y_agent.rb +119 -0
- data/lib/fix_a11y_generator.rb +32 -0
- data/package.json +11 -0
- data/trigger.txt +0 -0
- data/yarn.lock +1863 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 94ca19fae4a088d592146b8df934aa4b4a2b21f60e9318198ba79c3b0bff769c
|
4
|
+
data.tar.gz: 39c4d8da1ece45a3a350efaf5efecd69ed3986e0da435a7d03f256186a7dd89d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 594a27d14711f26226a383aa74f45bc2bd58d880c700edc21fcd70efbd51204c0acf432366eb0ec884ae6505ea6fcd34729b6f5b862508b373c4977ca6213624
|
7
|
+
data.tar.gz: 8947f736049078fe40be73c49325f45a2ce185c337c04ba979af45ae90dee33998d36107a89c4f2d461962d4c5aca23f6e9f839a513ef9c25da8a0d4c520c8ee
|
data/.env.sample
ADDED
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# A11y Agent
|
2
|
+
|
3
|
+
A11y Agent is a multi-turn AI agent that supports developers in remediating React templates. It uses rules from `eslint-plugin-jsx-a11y` from a [Sublayer](https://github.com/sublayerapp/sublayer) agent to perform iterative fixes, with user input along the way.
|
4
|
+
|
5
|
+
A11y Agent is unique in the way it combines traditional programming, AI prompting, and human judgement to accelerate the accessibility remediation process.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
1. Clone the repo:
|
10
|
+
|
11
|
+
git clone git@github.com:AccessLint/a11y-agent.git
|
12
|
+
|
13
|
+
2. Run the CLI command and follow the prompts:
|
14
|
+
|
15
|
+
ruby a11y_agent.rb /path/to/file.[jsx|tsx]
|
16
|
+
|
17
|
+
## Demo
|
18
|
+
|
19
|
+
https://github.com/user-attachments/assets/757227ad-7f8f-4fa8-933f-daea335313c9
|
20
|
+
|
data/eslint.config.mjs
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import globals from "globals";
|
2
|
+
import pluginJs from "@eslint/js";
|
3
|
+
import tseslint from "typescript-eslint";
|
4
|
+
import pluginReact from "eslint-plugin-react";
|
5
|
+
import jsxA11y from "eslint-plugin-jsx-a11y";
|
6
|
+
|
7
|
+
export default [
|
8
|
+
{
|
9
|
+
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
10
|
+
languageOptions: {
|
11
|
+
parserOptions: {
|
12
|
+
ecmaFeatures: {
|
13
|
+
jsx: true,
|
14
|
+
},
|
15
|
+
},
|
16
|
+
},
|
17
|
+
},
|
18
|
+
{ languageOptions: { globals: globals.browser } },
|
19
|
+
jsxA11y.flatConfigs.strict,
|
20
|
+
pluginJs.configs.recommended,
|
21
|
+
...tseslint.configs.recommended,
|
22
|
+
pluginReact.configs.flat.recommended,
|
23
|
+
];
|
data/fixtures/sample.tsx
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
import React from "react";
|
2
|
+
|
3
|
+
const AccessibilityViolations = () => {
|
4
|
+
return (
|
5
|
+
<div>
|
6
|
+
<html>
|
7
|
+
<head>
|
8
|
+
<title>Accessibility Violations</title>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<div>
|
12
|
+
<p>Here is an emoji not wrapped in a span: 😀</p>
|
13
|
+
|
14
|
+
<img src="image.jpg" alt="image" />
|
15
|
+
|
16
|
+
<a href="https://example.com">Click here</a>
|
17
|
+
|
18
|
+
<a href="#"></a>
|
19
|
+
|
20
|
+
<a href="invalid-url">Invalid Link</a>
|
21
|
+
|
22
|
+
<div aria-activedescendant="some-id"></div>
|
23
|
+
|
24
|
+
<div aria-bogus="true"></div>
|
25
|
+
|
26
|
+
<div aria-hidden="false"></div>
|
27
|
+
|
28
|
+
<div role="invalidRole"></div>
|
29
|
+
|
30
|
+
<input type="text" autoComplete="off" />
|
31
|
+
|
32
|
+
<div onClick={() => alert("Clicked!")}></div>
|
33
|
+
|
34
|
+
<input type="text" />
|
35
|
+
|
36
|
+
<h1></h1>
|
37
|
+
|
38
|
+
<iframe src="some-video.mp4"></iframe>
|
39
|
+
|
40
|
+
<img src="photo.jpg" alt="photo" />
|
41
|
+
|
42
|
+
<div onClick={() => alert("Clicked!")} tabIndex={-1}></div>
|
43
|
+
|
44
|
+
<label>Username</label>
|
45
|
+
<input type="text" id="username" />
|
46
|
+
|
47
|
+
<label htmlFor="username">Username</label>
|
48
|
+
<input type="text" id="username" />
|
49
|
+
|
50
|
+
<audio controls>
|
51
|
+
<source src="audio.mp3" type="audio/mp3" />
|
52
|
+
</audio>
|
53
|
+
|
54
|
+
<div onMouseOver={() => {}}></div>
|
55
|
+
|
56
|
+
<div accessKey="s"></div>
|
57
|
+
|
58
|
+
<div aria-hidden="true" tabIndex={0}></div>
|
59
|
+
|
60
|
+
<input type="text" autoFocus />
|
61
|
+
|
62
|
+
<marquee>Scrolling text</marquee>
|
63
|
+
|
64
|
+
<div role="button" onClick={() => {}}></div>
|
65
|
+
|
66
|
+
<div onClick={() => {}}></div>
|
67
|
+
|
68
|
+
<div role="link"></div>
|
69
|
+
|
70
|
+
<div tabIndex={1}></div>
|
71
|
+
|
72
|
+
<select onChange={() => {}}>
|
73
|
+
<option>Option 1</option>
|
74
|
+
<option>Option 2</option>
|
75
|
+
</select>
|
76
|
+
|
77
|
+
<div role="banner"></div>
|
78
|
+
|
79
|
+
<button role="button">Click me</button>
|
80
|
+
|
81
|
+
<table>
|
82
|
+
<tr>
|
83
|
+
<th>Header</th>
|
84
|
+
<th>Another Header</th>
|
85
|
+
</tr>
|
86
|
+
<tr>
|
87
|
+
<td>Data</td>
|
88
|
+
<td>More Data</td>
|
89
|
+
</tr>
|
90
|
+
</table>
|
91
|
+
|
92
|
+
<div tabIndex={5}></div>
|
93
|
+
</div>
|
94
|
+
</body>
|
95
|
+
</html>
|
96
|
+
</div>
|
97
|
+
);
|
98
|
+
};
|
99
|
+
|
100
|
+
export default AccessibilityViolations;
|
data/lib/a11y_agent.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'diffy'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'json'
|
6
|
+
require 'open3'
|
7
|
+
require 'rainbow/refinement'
|
8
|
+
require 'sublayer'
|
9
|
+
require 'tty-prompt'
|
10
|
+
require_relative './fix_a11y_generator'
|
11
|
+
|
12
|
+
Diffy::Diff.default_format = :color
|
13
|
+
|
14
|
+
# Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAi
|
15
|
+
# Sublayer.configuration.ai_model = "gpt-4o-mini"
|
16
|
+
|
17
|
+
# Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
|
18
|
+
# Sublayer.configuration.ai_model = "gemini-1.5-flash-latest"
|
19
|
+
|
20
|
+
Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
|
21
|
+
Sublayer.configuration.ai_model = 'claude-3-haiku-20240307'
|
22
|
+
|
23
|
+
module Sublayer
|
24
|
+
module Agents
|
25
|
+
class A11yAgent < Base
|
26
|
+
using Rainbow
|
27
|
+
|
28
|
+
def initialize(file:)
|
29
|
+
@accessibility_issues = []
|
30
|
+
@issue_types = []
|
31
|
+
@file = file
|
32
|
+
@file_contents = File.read(@file)
|
33
|
+
end
|
34
|
+
|
35
|
+
trigger_on_files_changed do
|
36
|
+
['./trigger.txt']
|
37
|
+
end
|
38
|
+
|
39
|
+
check_status do
|
40
|
+
puts "🔍 Checking accessibility issues in #{@file}..."
|
41
|
+
stdout, _stderr, _status = Open3.capture3("eslint #{@file} --format stylish")
|
42
|
+
|
43
|
+
@accessibility_issues = stdout.split("\n")[2..]
|
44
|
+
@accessibility_issues = @accessibility_issues.map { |issue| issue.gsub(/\s+/, ' ').strip }
|
45
|
+
|
46
|
+
puts "🚨 Found #{@accessibility_issues.length} accessibility issues" unless @accessibility_issues.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
goal_condition do
|
50
|
+
puts '🎉 All accessibility issues have been fixed!' if @accessibility_issues.empty?
|
51
|
+
@accessibility_issues.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
step do
|
55
|
+
prompt = TTY::Prompt.new
|
56
|
+
|
57
|
+
@accessibility_issues.each do |issue|
|
58
|
+
contents = File.read(@file)
|
59
|
+
|
60
|
+
user_input = nil
|
61
|
+
fixed = nil
|
62
|
+
additional_prompt = nil
|
63
|
+
|
64
|
+
until %i[yes no].include?(user_input)
|
65
|
+
puts "🔧 Attempting a fix: #{issue}"
|
66
|
+
result = FixA11yGenerator.new(contents:, issue:,
|
67
|
+
additional_prompt:).generate
|
68
|
+
result << "\n" unless result.end_with?("\n")
|
69
|
+
|
70
|
+
Diffy::Diff.new(contents, result).each_chunk do |chunk|
|
71
|
+
case chunk
|
72
|
+
when /^\+/
|
73
|
+
print chunk.to_s.green
|
74
|
+
when /^-/
|
75
|
+
print chunk.to_s.red
|
76
|
+
else
|
77
|
+
lines = chunk.to_s.split("\n")
|
78
|
+
puts lines[0..2].join("\n")
|
79
|
+
puts '...'
|
80
|
+
puts lines[-3..].join("\n")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
choices = [
|
85
|
+
{ key: 'y', name: 'approve and continue', value: :yes },
|
86
|
+
{ key: 'n', name: 'skip this change', value: :no },
|
87
|
+
{ key: 'r', name: 'retry with optional instructions', value: :retry },
|
88
|
+
{ key: 'q', name: 'quit; stop making changes', value: :quit }
|
89
|
+
]
|
90
|
+
|
91
|
+
user_input = prompt.expand('Approve changes?', choices)
|
92
|
+
|
93
|
+
case user_input
|
94
|
+
when :yes
|
95
|
+
fixed = result
|
96
|
+
when :no
|
97
|
+
fixed = contents
|
98
|
+
when :retry
|
99
|
+
additional_prompt = prompt.ask('Additional instructions:')
|
100
|
+
fixed = nil
|
101
|
+
when :quit
|
102
|
+
puts 'Quitting...'
|
103
|
+
exit
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
contents = fixed
|
108
|
+
|
109
|
+
puts 'Writing to file...'
|
110
|
+
File.write(@file, contents)
|
111
|
+
end
|
112
|
+
|
113
|
+
puts '🎉 Done!'
|
114
|
+
puts '✅ Complete diff:'
|
115
|
+
puts Diffy::Diff.new(@file_contents, File.read(@file))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class FixA11yGenerator < Sublayer::Generators::Base
|
2
|
+
llm_output_adapter type: :single_string,
|
3
|
+
name: "fix_accessibility_issue_based_on_a11y_issue",
|
4
|
+
description: "Given a JSX file and an accessibility issue, generate a new JSX file with the issue fixed."
|
5
|
+
|
6
|
+
def initialize(contents:, issue:, additional_prompt: nil)
|
7
|
+
@contents = contents
|
8
|
+
@issue = issue
|
9
|
+
@additional_prompt = additional_prompt
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def prompt
|
17
|
+
<<~PROMPT
|
18
|
+
Given the following JSX contents and an individual accessibility issue, generate a new JSX file with the individual issue fixed, leaving the rest of the contents unchanged.:
|
19
|
+
|
20
|
+
Code:
|
21
|
+
#{@contents}
|
22
|
+
|
23
|
+
Accessibility issue:
|
24
|
+
#{@issue}
|
25
|
+
|
26
|
+
Additional user instructions (if any):
|
27
|
+
#{@additional_prompt || "None"}
|
28
|
+
|
29
|
+
Return the JSX contents with the issue fixed.
|
30
|
+
PROMPT
|
31
|
+
end
|
32
|
+
end
|
data/package.json
ADDED
data/trigger.txt
ADDED
File without changes
|