a11y_agent 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ OPENAI_API_KEY=sk-proj-xxxxx
2
+ GEMINI_API_KEY=xxxxx
3
+ ANTHROPIC_API_KEY=sk-ant-xxxxx
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
+ ];
@@ -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;
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A11yAgent
4
+ VERSION = '0.0.1'
5
+ end
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
@@ -0,0 +1,11 @@
1
+ {
2
+ "devDependencies": {
3
+ "@eslint/js": "^9.8.0",
4
+ "eslint": "^9.8.0",
5
+ "eslint-plugin-jsx-a11y": "^6.9.0",
6
+ "eslint-plugin-react": "^7.35.0",
7
+ "globals": "^15.8.0",
8
+ "typescript": "^5.5.4",
9
+ "typescript-eslint": "^7.17.0"
10
+ }
11
+ }
data/trigger.txt ADDED
File without changes