91cb77b
commit 91cb77b
11 files changed
+400 -18
Showing 11 changed files+400-18
opengrammar/CONTRIBUTING.mdopengrammar/README.mdopengrammar/backend/src/analyzer.tsopengrammar/backend/src/index.tsopengrammar/backend/src/rules/context-filter.tsopengrammar/backend/src/shared-types.tsopengrammar/backend/tests/engine.test.tsopengrammar/extension/src/background/index.tsopengrammar/extension/src/options/index.htmlopengrammar/extension/src/options/options.tsxopengrammar/extension/src/types.ts
βΌopengrammar/CONTRIBUTING.md
+124-0
| @@ -0,0 +1,124 @@ | ||
1 | +# Contributing to OpenGrammar | |
2 | + | |
3 | +Thank you for your interest in contributing! Whether you're adding a new grammar rule, refining the AI context, or building out a new front-end UI feature, we are incredibly excited to welcome you. | |
4 | + | |
5 | +This document outlines everything you need to know about the engine structure. | |
6 | + | |
7 | +--- | |
8 | + | |
9 | +## π οΈ Local Development | |
10 | + | |
11 | +### Prerequisites | |
12 | + | |
13 | +- [Bun](https://bun.sh/) (>= 1.3) | |
14 | +- Node.js (>= 20) for the extension front-end | |
15 | + | |
16 | +### Starting the Environment | |
17 | + | |
18 | +The project is split into the `/backend` (Hono API) and `/extension` (Google Chrome frontend). | |
19 | + | |
20 | +1. **Spin up the Backend:** | |
21 | + ```bash | |
22 | + cd backend | |
23 | + bun install | |
24 | + bun dev | |
25 | + ``` | |
26 | + | |
27 | +2. **Spin up the Extension:** | |
28 | + ```bash | |
29 | + cd extension | |
30 | + npm install | |
31 | + npm run build # (Vite output goes to /extension/dist) | |
32 | + npm run dev # (To auto-recompile UI edits) | |
33 | + ``` | |
34 | + | |
35 | +### Running Tests | |
36 | + | |
37 | +We have strict performance and validation checks. Before pushing anywhere, run: | |
38 | + | |
39 | +```bash | |
40 | +cd backend | |
41 | +bun test | |
42 | +bun run bench.ts | |
43 | +``` | |
44 | + | |
45 | +The 814+ rule engine should comfortably process 10,000 words in under a second (`1000ms`). | |
46 | + | |
47 | +--- | |
48 | + | |
49 | +## π The Rule Engine Architecture | |
50 | + | |
51 | +OpenGrammar is fundamentally an offline-first system powered by the huge `CORE_RULES` library in `backend/src/rules/index.ts`. | |
52 | + | |
53 | +It runs `RegexRules` and `NLPRules` to check every single chunk of a sentence. Before adding a new rule, search through the modules (`adjectives-adverbs.ts`, `style-tone.ts`, etc.) to see if your logical check fits an existing category. | |
54 | + | |
55 | +### How to Add a Regex Rule | |
56 | +Regex rules are incredibly fast but lack contextual awareness (they don't know the difference between "a lead pipe" and "to lead a horse"). Write these when the error is deterministic. | |
57 | + | |
58 | +**Example `backend/src/rules/categories/punctuation.ts`:** | |
59 | +```typescript | |
60 | +{ | |
61 | + id: 'PU_double_spaces', | |
62 | + type: 'regex', | |
63 | + pattern: '\\b[a-zA-Z]+\\s{2,}[a-zA-Z]+\\b', | |
64 | + check: (text: string) => { | |
65 | + return createRegexRule( | |
66 | + 'PU_double_spaces', | |
67 | + /(\S)(\s{2,})(\S)/g, | |
68 | + 'grammar', | |
69 | + '', // replacement dynamically handled below: | |
70 | + 'Avoid double spaces between words.', | |
71 | + text | |
72 | + ).map(issue => ({ | |
73 | + ...issue, | |
74 | + suggestion: issue.original.replace(/\s{2,}/g, ' '), | |
75 | + priority: 85 | |
76 | + })); | |
77 | + } | |
78 | +} | |
79 | +``` | |
80 | + | |
81 | +### How to Add an NLP Rule | |
82 | +Using the `compromise` NLP library, we tag parts-of-speech (Verbs, Nouns, Adjectives). Use NLP rules when the phrasing is ambiguous and relies on grammar syntax. | |
83 | + | |
84 | +**Example `backend/src/rules/categories/verb-tense.ts`:** | |
85 | +```typescript | |
86 | +{ | |
87 | + id: 'VT_stative_progressives', | |
88 | + type: 'nlp', | |
89 | + check: (text: string, doc: any) => { | |
90 | + const issues: Issue[] = []; | |
91 | + // Only target Present Participle Gerunds of stative verbs | |
92 | + const matches = doc.match('(is|am|are|was|were) (knowing|belonging|desiring|preferring|liking|loving|hating)'); | |
93 | + | |
94 | + matches.forEach((m: any) => { | |
95 | + const original = m.text(); | |
96 | + // Heuristically map "is knowing" -> "knows" | |
97 | + issues.push({ | |
98 | + id: 'VT_stative_progressives', | |
99 | + type: 'grammar', | |
100 | + original, | |
101 | + suggestion: fixStativeVerb(original), | |
102 | + reason: 'Stative verbs like this generally do not take the progressive (-ing) tense.', | |
103 | + offset: m.out('offset').start, | |
104 | + length: original.length, | |
105 | + priority: 90 | |
106 | + }); | |
107 | + }); | |
108 | + return issues; | |
109 | + } | |
110 | +} | |
111 | +``` | |
112 | + | |
113 | +--- | |
114 | + | |
115 | +## π Pull Request Guidelines | |
116 | + | |
117 | +When submitting a PR: | |
118 | +1. Include passing tests in `engine.test.ts`. Every single rule MUST have at least one valid and invalid test assertion. | |
119 | +2. If introducing a `style` rule or `business-writing` check, make sure its `id` follows the appropriate `MODULE_PREFIX` mapping inside `context-filter.ts`. Otherwise, the context engine won't know when to disable it in casual chat apps! | |
120 | +3. Prefix your commits with `feat:`, `fix:`, or `chore:` following conventional commits. | |
121 | + | |
122 | +We use **Biome** on GitHub Actions to auto-lint against the core codebase formatting (2 spaces, single quotes). You don't need to format it perfectly yourself, just ensure the build isn't spectacularly broken. | |
123 | + | |
124 | +Welcome to OpenGrammar! π | |
0 | 125 | |
βΌopengrammar/README.md
+92-0
| @@ -0,0 +1,92 @@ | ||
1 | +<div align="center"> | |
2 | + <img src="https://raw.githubusercontent.com/lucide-icons/lucide/main/icons/feather.svg" width="80" height="80" alt="OpenGrammar Logo" /> | |
3 | + | |
4 | + <h1>OpenGrammar</h1> | |
5 | + | |
6 | + <p><strong>A blazingly fast, privacy-first, open-source grammar and writing engine.</strong></p> | |
7 | + | |
8 | + <p> | |
9 | + <a href="https://github.com/swadhinbiswas/opengrammar/actions"><img src="https://github.com/swadhinbiswas/opengrammar/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a> | |
10 | + <a href="https://codefactor.io/repository/github/swadhinbiswas/opengrammar"><img src="https://www.codefactor.io/repository/github/swadhinbiswas/opengrammar/badge" alt="CodeFactor" /></a> | |
11 | + <a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License"></a> | |
12 | + <img src="https://img.shields.io/badge/Rules-814+-success" alt="Rules Active"> | |
13 | + </p> | |
14 | +</div> | |
15 | + | |
16 | +--- | |
17 | + | |
18 | +**OpenGrammar** is an advanced grammar, style, and spell-checking engine built to be completely self-hosted. Unlike commercial exact-match grammar tools that vacuum up your keystrokes, OpenGrammar processes text either locally using deterministic Regex/NLP rules, or through an LLM provider of your choice (OpenAI, Local Ollama, Groq, OpenRouter). | |
19 | + | |
20 | +## β¨ Features | |
21 | + | |
22 | +- β‘ **Blazingly Fast Engine**: Core engine analyzes 10,000 words in **< 650ms**. | |
23 | +- π§ **Hybrid Architecture**: Combines ultra-fast Regex pattern matching with `compromise` NLP for intelligent syntax analysis. | |
24 | +- π **Massive Rule Library**: Ships with **814+ built-in rules** spanning: | |
25 | + - Subject-Verb Agreement, Irregular Verbs, Subjunctives | |
26 | + - Business Writing, Academic Tone, Inclusive Language | |
27 | + - Clarity, Redundancy, Readability, and ClichΓ©s | |
28 | +- π― **Context-Aware Filtering**: Automatically detects where you are writing (Slack = Chat, GitHub = Technical, Gmail = Email) and dynamically turns off nagging style rules when you're just typing casually. | |
29 | +- ποΈ **Granular Toggles**: Fully configurable rules via the Chrome Extension UI. | |
30 | +- βοΈ **Bring Your Own LLM**: Optionally hook up OpenAI, Groq, or a completely offline local **Ollama** model for deep contextual analysis and AI sentence rewriting. | |
31 | + | |
32 | +## π Getting Started | |
33 | + | |
34 | +OpenGrammar is split into two parts: an **API Backend** (built with Hono + Bun) and a **Chrome Extension** (built with React + Vite). | |
35 | + | |
36 | +### 1. Start the Backend | |
37 | + | |
38 | +The engine runs on a lightweight [Hono](https://hono.dev/) server using [Bun](https://bun.sh/). | |
39 | + | |
40 | +```bash | |
41 | +cd backend | |
42 | +bun install | |
43 | +bun run dev | |
44 | +``` | |
45 | +*Server starts on `http://localhost:8787`* | |
46 | + | |
47 | +### 2. Load the Chrome Extension | |
48 | + | |
49 | +```bash | |
50 | +cd extension | |
51 | +npm install | |
52 | +npm run build | |
53 | +``` | |
54 | + | |
55 | +Then load it into your browser: | |
56 | +1. Open Chrome and navigate to `chrome://extensions/` | |
57 | +2. Enable **Developer mode** (top right toggle). | |
58 | +3. Click **Load unpacked** and select the `/extension/dist` folder. | |
59 | + | |
60 | +--- | |
61 | + | |
62 | +## βοΈ Deployment | |
63 | + | |
64 | +The backend is built on standard Web APIs and runs perfectly on edge networks. | |
65 | + | |
66 | +### Deploy to Cloudflare Workers (Recommended) | |
67 | +```bash | |
68 | +cd backend | |
69 | +npm run deploy | |
70 | +``` | |
71 | +*This uses `wrangler` to instantly deploy your engine globally via Cloudflare.* | |
72 | + | |
73 | +--- | |
74 | + | |
75 | +## π οΈ Performance Architecture | |
76 | + | |
77 | +OpenGrammar prioritizes offline, instant feedback without the latency of an AI roundtrip: | |
78 | +1. **Spellchecker Segment**: Loads a custom dictionary tree (120k+ words) for instant typo detection. | |
79 | +2. **NLP Tagger Segment**: `compromise` tags parts of speech (Nouns, Verbs, Quotations) to prevent false positives. | |
80 | +3. **Regex Engine**: Matches 800+ deterministic error patterns in a single pass. | |
81 | +4. **Deduplicator**: Ranks overlapping issues (Grammar > Spelling > Clarity > Style) and drops lower-priority items. | |
82 | + | |
83 | +## π€ Contributing | |
84 | + | |
85 | +We welcome contributions of all kinds! The heartbeat of this project is its `CORE_RULES` rule registry. | |
86 | +Adding a new grammar or style rule is as easy as writing a regex expression in TypeScript. | |
87 | + | |
88 | +Please read our [Contributing Guide](CONTRIBUTING.md) to learn how the engine handles rules, how to write tests, and how to submit a PR! | |
89 | + | |
90 | +## π License | |
91 | + | |
92 | +Apache 2.0 License - see the [LICENSE](LICENSE) file for details. | |
0 | 93 | |
βΌopengrammar/backend/src/analyzer.ts
+9-6
| @@ -35,7 +35,7 @@ export class RuleBasedAnalyzer { | ||
35 | 35 | private static dictionary: Set<string> = new Set(); |
36 | 36 | private static customRules: CustomRule[] = []; |
37 | 37 | |
38 | - static analyze(text: string, options?: { dictionary?: string[]; customRules?: CustomRule[]; writingContext?: WritingContext }): Issue[] { | |
38 | + static analyze(text: string, options?: { dictionary?: string[]; customRules?: CustomRule[]; writingContext?: WritingContext; disabledModules?: string[] }): Issue[] { | |
39 | 39 | const issues: Issue[] = []; |
40 | 40 | |
41 | 41 | if (options?.dictionary) { |
| @@ -46,8 +46,11 @@ export class RuleBasedAnalyzer { | ||
46 | 46 | this.customRules = options.customRules; |
47 | 47 | } |
48 | 48 | |
49 | - // Dictionary-based spell checking (real spell checker) | |
50 | - issues.push(...checkSpelling(text, this.dictionary)); | |
49 | + // Dictionary-based spell checking (unless spelling module is disabled) | |
50 | + const isSpellingDisabled = options?.disabledModules?.some(m => m.toLowerCase() === 'spelling'); | |
51 | + if (!isSpellingDisabled) { | |
52 | + issues.push(...checkSpelling(text, this.dictionary)); | |
53 | + } | |
51 | 54 | |
52 | 55 | // Initialize NLP Engine for Syntax Checks |
53 | 56 | let doc: any = null; |
| @@ -57,9 +60,9 @@ export class RuleBasedAnalyzer { | ||
57 | 60 | console.warn('NLP Engine parsing disabled or failed:', e); |
58 | 61 | } |
59 | 62 | |
60 | - // Run Modular CORE RULES (filtered by writing context) | |
61 | - const activeRules = options?.writingContext | |
62 | - ? filterRulesByContext(CORE_RULES, options.writingContext) | |
63 | + // Run Modular CORE RULES (filtered by writing context and manual overrides) | |
64 | + const activeRules = options?.writingContext || options?.disabledModules | |
65 | + ? filterRulesByContext(CORE_RULES, options.writingContext || 'general', options.disabledModules) | |
63 | 66 | : CORE_RULES; |
64 | 67 | |
65 | 68 | for (const rule of activeRules) { |
66 | 69 | |
βΌopengrammar/backend/src/index.ts
+2-2
| @@ -88,7 +88,7 @@ app.post('/analyze', async (c) => { | ||
88 | 88 | |
89 | 89 | try { |
90 | 90 | const body: AnalyzeRequest = await c.req.json(); |
91 | - const { text, apiKey, model, provider, baseUrl, context } = body; | |
91 | + const { text, apiKey, model, provider, baseUrl, context, disabledModules } = body; | |
92 | 92 | |
93 | 93 | // Validate input |
94 | 94 | if (!text || typeof text !== 'string') { |
| @@ -103,7 +103,7 @@ app.post('/analyze', async (c) => { | ||
103 | 103 | const writingContext = detectWritingContext(context?.domain); |
104 | 104 | |
105 | 105 | // Run rule-based analysis with context-aware filtering |
106 | - const ruleIssues = RuleBasedAnalyzer.analyze(text, { writingContext }); | |
106 | + const ruleIssues = RuleBasedAnalyzer.analyze(text, { writingContext, disabledModules }); | |
107 | 107 | let issues = enrichIssues(ruleIssues, 'rule', text, context); |
108 | 108 | |
109 | 109 | // Run LLM analysis if API key provided or using local Ollama |
110 | 110 | |
βΌopengrammar/backend/src/rules/context-filter.ts
+39-8
| @@ -132,20 +132,51 @@ function getRuleModule(ruleId: string): string { | ||
132 | 132 | } |
133 | 133 | |
134 | 134 | /** |
135 | - * Filter rules based on writing context. | |
136 | - * Returns only the rules that are relevant for the given context. | |
135 | + * Filter rules based on writing context and user-defined disabled modules. | |
136 | + * Returns only the rules that are relevant for the given context and not manually disabled. | |
137 | 137 | */ |
138 | -export function filterRulesByContext(rules: Rule[], context: WritingContext): Rule[] { | |
139 | - if (context === 'general' || context === 'document') { | |
140 | - return rules; // No filtering β all rules active | |
138 | +export function filterRulesByContext(rules: Rule[], context: WritingContext, disabledModules?: string[]): Rule[] { | |
139 | + // 1. First, apply manual user overrides (stripping out entire categories if disabled) | |
140 | + // We map the UI categories (Grammar, Spelling, Punctuation, Style, Clarity) | |
141 | + // to the internal MODULE_PREFIXES groups. | |
142 | + const disabledPrefixGroups = new Set<string>(); | |
143 | + | |
144 | + if (disabledModules && disabledModules.length > 0) { | |
145 | + const dLower = disabledModules.map(d => d.toLowerCase()); | |
146 | + if (dLower.includes('grammar')) { | |
147 | + ['grammar', 'grammar-advanced', 'sentence-structure', 'verb-tense', 'nouns-pronouns', 'adjectives-adverbs', 'prepositions', 'conjunctions'].forEach(m => disabledPrefixGroups.add(m)); | |
148 | + } | |
149 | + if (dLower.includes('spelling')) { | |
150 | + ['spelling', 'spelling-advanced', 'confused-words'].forEach(m => disabledPrefixGroups.add(m)); | |
151 | + } | |
152 | + if (dLower.includes('punctuation')) { | |
153 | + ['punctuation', 'capitalization'].forEach(m => disabledPrefixGroups.add(m)); | |
154 | + } | |
155 | + if (dLower.includes('style')) { | |
156 | + ['style', 'style-tone', 'formality', 'academic-writing', 'business-writing', 'inclusive-language', 'formatting-idioms'].forEach(m => disabledPrefixGroups.add(m)); | |
157 | + } | |
158 | + if (dLower.includes('clarity')) { | |
159 | + ['clarity', 'readability'].forEach(m => disabledPrefixGroups.add(m)); | |
160 | + } | |
141 | 161 | } |
142 | 162 | |
143 | - const enabledModules = CONTEXT_MODULES[context]; | |
144 | - if (!enabledModules) return rules; | |
163 | + // 2. Map standard context | |
164 | + const enabledContextModules = CONTEXT_MODULES[context]; | |
145 | 165 | |
146 | 166 | return rules.filter(rule => { |
147 | 167 | const module = getRuleModule(rule.id); |
148 | - return module === 'general' || enabledModules.has(module); | |
168 | + | |
169 | + // Manual override check: if the user disabled this entire module group, strip it out. | |
170 | + if (disabledPrefixGroups.has(module)) { | |
171 | + return false; | |
172 | + } | |
173 | + | |
174 | + // Context filter check | |
175 | + if (context === 'general' || context === 'document') { | |
176 | + return true; // No context filtering | |
177 | + } | |
178 | + | |
179 | + return module === 'general' || (enabledContextModules && enabledContextModules.has(module)); | |
149 | 180 | }); |
150 | 181 | } |
151 | 182 | |
152 | 183 | |
βΌopengrammar/backend/src/shared-types.ts
+1-0
| @@ -31,6 +31,7 @@ export interface AnalyzeRequest { | ||
31 | 31 | customRules?: CustomRule[]; |
32 | 32 | dictionary?: string[]; |
33 | 33 | context?: AnalysisContext; |
34 | + disabledModules?: string[]; | |
34 | 35 | } |
35 | 36 | |
36 | 37 | export interface CustomRule { |
37 | 38 | |
βΌopengrammar/backend/tests/engine.test.ts
+32-0
| @@ -627,3 +627,35 @@ describe('Context Detection', () => { | ||
627 | 627 | expect(detectWritingContext(undefined)).toBe('general'); |
628 | 628 | }); |
629 | 629 | }); |
630 | + | |
631 | +// ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
632 | +// Manual Rule Overrides (disabledModules) | |
633 | +// ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
634 | +describe('Manual Rule Filtering (disabledModules)', () => { | |
635 | + const testText = 'Gonna go btw. He buyed a car. The chairman spoke. I think that this is true.'; | |
636 | + | |
637 | + test('disabling style rules ignores "gonna" and "btw"', () => { | |
638 | + // Usually 'general' flags everything. If we disable 'style', only grammar and spelling run. | |
639 | + const issues = RuleBasedAnalyzer.analyze(testText, { | |
640 | + writingContext: 'general', | |
641 | + disabledModules: ['style'], // 'style' covers style, formality, etc. | |
642 | + }); | |
643 | + | |
644 | + // Should still catch 'buyed' (grammar) | |
645 | + expect(issues.some(i => i.original.toLowerCase() === 'buyed')).toBe(true); | |
646 | + // Should NOT catch 'gonna' (style/formality) | |
647 | + expect(issues.some(i => i.original.toLowerCase() === 'gonna')).toBe(false); | |
648 | + }); | |
649 | + | |
650 | + test('disabling grammar ignores grammar-specific "buyed"', () => { | |
651 | + const issues = RuleBasedAnalyzer.analyze(testText, { | |
652 | + writingContext: 'general', | |
653 | + disabledModules: ['grammar'], | |
654 | + }); | |
655 | + | |
656 | + // Should NOT catch 'buyed' as a grammar issue (though spellchecker still flags it) | |
657 | + expect(issues.some(i => i.original.toLowerCase() === 'buyed' && i.type === 'grammar')).toBe(false); | |
658 | + // Might still catch 'gonna' (style) | |
659 | + expect(issues.some(i => i.original.toLowerCase() === 'gonna')).toBe(true); | |
660 | + }); | |
661 | +}); | |
630 | 662 | |
βΌopengrammar/extension/src/background/index.ts
+5-2
| @@ -220,7 +220,8 @@ async function handleGrammarCheck( | ||
220 | 220 | dictionary, |
221 | 221 | backendUrl, |
222 | 222 | provider, |
223 | - customBaseUrl | |
223 | + customBaseUrl, | |
224 | + disabledModules | |
224 | 225 | } = await chrome.storage.sync.get([ |
225 | 226 | 'apiKey', |
226 | 227 | 'model', |
| @@ -229,7 +230,8 @@ async function handleGrammarCheck( | ||
229 | 230 | 'dictionary', |
230 | 231 | 'backendUrl', |
231 | 232 | 'provider', |
232 | - 'customBaseUrl' | |
233 | + 'customBaseUrl', | |
234 | + 'disabledModules' | |
233 | 235 | ]); |
234 | 236 | |
235 | 237 | if (enabled === false) { |
| @@ -251,6 +253,7 @@ async function handleGrammarCheck( | ||
251 | 253 | ignoredIssues: ignoredIssues || [], |
252 | 254 | dictionary: dictionary || [], |
253 | 255 | context, |
256 | + disabledModules: disabledModules || [], | |
254 | 257 | }; |
255 | 258 | |
256 | 259 | const response = await fetch(`${baseUrl}/analyze`, { |
257 | 260 | |
βΌopengrammar/extension/src/options/index.html
+63-0
| @@ -71,6 +71,69 @@ | ||
71 | 71 | </div> |
72 | 72 | </section> |
73 | 73 | |
74 | + <!-- Rule Configurations --> | |
75 | + <section class="settings-section"> | |
76 | + <h2>Writing Preferences</h2> | |
77 | + <p class="section-description"> | |
78 | + Customize which categories of errors OpenGrammar looks for. By default, everything is enabled. | |
79 | + </p> | |
80 | + | |
81 | + <div class="setting-group"> | |
82 | + <label class="toggle-setting"> | |
83 | + <div class="toggle-info"> | |
84 | + <span class="setting-label">Grammar</span> | |
85 | + <span class="setting-description">Subject-verb agreement, irregular verbs, pronouns</span> | |
86 | + </div> | |
87 | + <input type="checkbox" id="checkGrammar" class="toggle-input" checked> | |
88 | + <span class="toggle-slider"></span> | |
89 | + </label> | |
90 | + </div> | |
91 | + | |
92 | + <div class="setting-group"> | |
93 | + <label class="toggle-setting"> | |
94 | + <div class="toggle-info"> | |
95 | + <span class="setting-label">Spelling</span> | |
96 | + <span class="setting-description">Typos, misspelled words, confused words</span> | |
97 | + </div> | |
98 | + <input type="checkbox" id="checkSpelling" class="toggle-input" checked> | |
99 | + <span class="toggle-slider"></span> | |
100 | + </label> | |
101 | + </div> | |
102 | + | |
103 | + <div class="setting-group"> | |
104 | + <label class="toggle-setting"> | |
105 | + <div class="toggle-info"> | |
106 | + <span class="setting-label">Punctuation</span> | |
107 | + <span class="setting-description">Missing commas, incorrect hyphens</span> | |
108 | + </div> | |
109 | + <input type="checkbox" id="checkPunctuation" class="toggle-input" checked> | |
110 | + <span class="toggle-slider"></span> | |
111 | + </label> | |
112 | + </div> | |
113 | + | |
114 | + <div class="setting-group"> | |
115 | + <label class="toggle-setting"> | |
116 | + <div class="toggle-info"> | |
117 | + <span class="setting-label">Style & Tone</span> | |
118 | + <span class="setting-description">Formality, academic writing, business etiquette</span> | |
119 | + </div> | |
120 | + <input type="checkbox" id="checkStyle" class="toggle-input" checked> | |
121 | + <span class="toggle-slider"></span> | |
122 | + </label> | |
123 | + </div> | |
124 | + | |
125 | + <div class="setting-group"> | |
126 | + <label class="toggle-setting"> | |
127 | + <div class="toggle-info"> | |
128 | + <span class="setting-label">Clarity</span> | |
129 | + <span class="setting-description">Wordiness, redundant phrasing, readability</span> | |
130 | + </div> | |
131 | + <input type="checkbox" id="checkClarity" class="toggle-input" checked> | |
132 | + <span class="toggle-slider"></span> | |
133 | + </label> | |
134 | + </div> | |
135 | + </section> | |
136 | + | |
74 | 137 | <!-- API Settings --> |
75 | 138 | <section class="settings-section"> |
76 | 139 | <h2>AI Settings</h2> |
77 | 140 | |
βΌopengrammar/extension/src/options/options.tsx
+32-0
| @@ -11,6 +11,7 @@ interface Settings { | ||
11 | 11 | disabledDomains: string[]; |
12 | 12 | dictionary: string[]; |
13 | 13 | ignoredIssues: IgnoredIssue[]; |
14 | + disabledModules: string[]; | |
14 | 15 | } |
15 | 16 | |
16 | 17 | // DOM Elements |
| @@ -19,6 +20,11 @@ const elements = { | ||
19 | 20 | checkAsYouType: document.getElementById('checkAsYouType') as HTMLInputElement, |
20 | 21 | showNotifications: document.getElementById('showNotifications') as HTMLInputElement, |
21 | 22 | autocompleteEnabled: document.getElementById('autocompleteEnabled') as HTMLInputElement, |
23 | + checkGrammar: document.getElementById('checkGrammar') as HTMLInputElement, | |
24 | + checkSpelling: document.getElementById('checkSpelling') as HTMLInputElement, | |
25 | + checkPunctuation: document.getElementById('checkPunctuation') as HTMLInputElement, | |
26 | + checkStyle: document.getElementById('checkStyle') as HTMLInputElement, | |
27 | + checkClarity: document.getElementById('checkClarity') as HTMLInputElement, | |
22 | 28 | apiKey: document.getElementById('apiKey') as HTMLInputElement, |
23 | 29 | toggleApiKey: document.getElementById('toggleApiKey') as HTMLButtonElement, |
24 | 30 | model: document.getElementById('model') as HTMLSelectElement, |
| @@ -58,6 +64,7 @@ let settings: Settings = { | ||
58 | 64 | disabledDomains: [], |
59 | 65 | dictionary: [], |
60 | 66 | ignoredIssues: [], |
67 | + disabledModules: [], | |
61 | 68 | }; |
62 | 69 | |
63 | 70 | let analyticsSummary: AnalyticsSummary | null = null; |
| @@ -96,6 +103,7 @@ async function loadSettings() { | ||
96 | 103 | disabledDomains: result.disabledDomains || [], |
97 | 104 | dictionary: result.dictionary || [], |
98 | 105 | ignoredIssues: normalizeIgnoredIssues(result.ignoredIssues), |
106 | + disabledModules: result.disabledModules || [], | |
99 | 107 | }; |
100 | 108 | |
101 | 109 | // Update UI |
| @@ -103,6 +111,11 @@ async function loadSettings() { | ||
103 | 111 | elements.checkAsYouType.checked = settings.checkAsYouType; |
104 | 112 | elements.showNotifications.checked = settings.showNotifications; |
105 | 113 | elements.autocompleteEnabled.checked = settings.autocompleteEnabled; |
114 | + elements.checkGrammar.checked = !settings.disabledModules.includes('grammar'); | |
115 | + elements.checkSpelling.checked = !settings.disabledModules.includes('spelling'); | |
116 | + elements.checkPunctuation.checked = !settings.disabledModules.includes('punctuation'); | |
117 | + elements.checkStyle.checked = !settings.disabledModules.includes('style'); | |
118 | + elements.checkClarity.checked = !settings.disabledModules.includes('clarity'); | |
106 | 119 | elements.apiKey.value = settings.apiKey; |
107 | 120 | elements.model.value = settings.model; |
108 | 121 | elements.backendUrl.value = settings.backendUrl; |
| @@ -164,6 +177,7 @@ async function saveSettings() { | ||
164 | 177 | disabledDomains: settings.disabledDomains, |
165 | 178 | dictionary: settings.dictionary, |
166 | 179 | ignoredIssues: settings.ignoredIssues, |
180 | + disabledModules: settings.disabledModules, | |
167 | 181 | }, |
168 | 182 | () => { |
169 | 183 | console.log('Settings saved'); |
| @@ -198,6 +212,24 @@ function setupEventListeners() { | ||
198 | 212 | saveSettings(); |
199 | 213 | }); |
200 | 214 | |
215 | + // Rule Category Toggles | |
216 | + const updateDisabledModules = () => { | |
217 | + const disabled: string[] = []; | |
218 | + if (!elements.checkGrammar.checked) disabled.push('grammar'); | |
219 | + if (!elements.checkSpelling.checked) disabled.push('spelling'); | |
220 | + if (!elements.checkPunctuation.checked) disabled.push('punctuation'); | |
221 | + if (!elements.checkStyle.checked) disabled.push('style'); | |
222 | + if (!elements.checkClarity.checked) disabled.push('clarity'); | |
223 | + settings.disabledModules = disabled; | |
224 | + saveSettings(); | |
225 | + }; | |
226 | + | |
227 | + elements.checkGrammar.addEventListener('change', updateDisabledModules); | |
228 | + elements.checkSpelling.addEventListener('change', updateDisabledModules); | |
229 | + elements.checkPunctuation.addEventListener('change', updateDisabledModules); | |
230 | + elements.checkStyle.addEventListener('change', updateDisabledModules); | |
231 | + elements.checkClarity.addEventListener('change', updateDisabledModules); | |
232 | + | |
201 | 233 | // API Key |
202 | 234 | elements.apiKey.addEventListener('input', () => { |
203 | 235 | settings.apiKey = elements.apiKey.value; |
204 | 236 | |
βΌopengrammar/extension/src/types.ts
+1-0
| @@ -46,6 +46,7 @@ export interface AnalyzeRequest { | ||
46 | 46 | ignoredIssues?: string[]; |
47 | 47 | dictionary?: string[]; |
48 | 48 | context?: AnalysisContext; |
49 | + disabledModules?: string[]; | |
49 | 50 | } |
50 | 51 | |
51 | 52 | export interface AnalysisContext { |
52 | 53 | |