Smart grammar fixing with LanguageTool and language detection
目前為
// ==UserScript==
// @name Smart Grammar Fixer Pro
// @namespace http://tampermonkey.net/
// @version 6.1
// @description Smart grammar fixing with LanguageTool and language detection
// @author You
// @match *://*/*
// @exclude https://docs.google.com/*
// @exclude https://*.office.com/*
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant unsafeWindow
// @connect api.languagetool.org
// @connect ws.detectlanguage.com
// @antifeature none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// API Configuration
const API_CONFIG = {
languagetool: 'https://api.languagetool.org/v2/check',
detectlanguage: 'https://ws.detectlanguage.com/0.2/detect'
};
// Language Support
const LANGUAGE_SUPPORT = {
'en-US': 'English (US)',
'en-GB': 'English (GB)',
'ar': 'Arabic',
'de-DE': 'German',
'fr-FR': 'French',
'es-ES': 'Spanish',
'it-IT': 'Italian',
'pt-PT': 'Portuguese',
'pt-BR': 'Portuguese (BR)',
'nl-NL': 'Dutch',
'ru-RU': 'Russian',
'ja-JP': 'Japanese',
'zh-CN': 'Chinese',
'ko-KR': 'Korean',
'pl-PL': 'Polish',
'sv-SE': 'Swedish',
'da-DK': 'Danish',
'fi-FI': 'Finnish',
'no-NO': 'Norwegian',
'tr-TR': 'Turkish'
};
// Default settings
const DEFAULT_SETTINGS = {
// Core Behavior
enabled: true,
debugMode: false,
// API Keys
apiKeys: {
detectlanguage: 'a40f80d21131976bdedf653088a12ce0'
},
// Language Configuration
language: {
main: 'en-US',
fallback: 'en-US',
autoDetect: true,
confidenceThreshold: 0.7,
forceLanguage: false,
correctionLanguage: 'auto' // New: Choose language for correction
},
// Correction Settings
correction: {
autoFixOnSend: true,
minTextLength: 3,
maxTextLength: 5000,
fixPunctuation: true,
fixCapitalization: true,
aggressiveCorrection: false
},
// User Interface
ui: {
showIcons: true,
showNotifications: true,
iconPosition: 'top-right',
iconSize: 'medium',
darkMode: 'auto',
animations: true // New: Enable animations
},
// Shortcuts
shortcuts: {
smartFix: 'Alt+A',
fixAndSend: 'Alt+Enter',
quickFix: 'Alt+Q',
toggleEnabled: 'Alt+Shift+G',
openSettings: 'Alt+Shift+S'
},
// Domain-specific Rules
domainRules: {
'gmail.com': { autoFixOnSend: true },
'outlook.com': { autoFixOnSend: true },
'twitter.com': { minTextLength: 5 },
'facebook.com': { minTextLength: 10 },
'chat.openai.com': { enabled: false },
'docs.google.com': { enabled: false }
}
};
let settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
let isProcessing = false;
let currentDomain = window.location.hostname;
// Inject styles
function injectStyles() {
const styles = `
.grammar-helper-icon {
position: absolute;
width: 24px;
height: 24px;
background: #4CAF50;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: bold;
cursor: pointer;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: all 0.3s ease;
border: 2px solid white;
}
.grammar-helper-icon:hover {
transform: scale(1.2);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.grammar-language-badge {
position: absolute;
top: -5px;
right: -5px;
background: #2196F3;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
font-size: 9px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid white;
font-weight: bold;
}
/* Enhanced notification animations */
.grammar-notification {
position: absolute;
background: #4CAF50;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-family: Arial, sans-serif;
z-index: 10002;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 200px;
white-space: nowrap;
transform: translateY(-20px);
opacity: 0;
}
.grammar-notification.error {
background: #f44336;
}
.grammar-notification.warning {
background: #FF9800;
}
.grammar-notification.info {
background: #2196F3;
}
.grammar-notification.processing {
background: #9C27B0;
}
.grammar-notification.success {
background: #4CAF50;
}
/* Animation classes */
.grammar-notification.show {
animation: slideInBounce 0.5s ease-out forwards;
}
.grammar-notification.hide {
animation: slideOutUp 0.3s ease-in forwards;
}
/* Processing animation */
.grammar-notification.processing::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: rgba(255,255,255,0.7);
border-radius: 0 0 6px 6px;
animation: processingBar 2s linear infinite;
}
@keyframes slideInBounce {
0% {
transform: translateY(-20px);
opacity: 0;
}
60% {
transform: translateY(5px);
opacity: 1;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideOutUp {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
@keyframes processingBar {
0% {
width: 0%;
}
50% {
width: 100%;
}
100% {
width: 0%;
}
}
/* Pulse animation for icon when processing */
.grammar-helper-icon.processing {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
50% {
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
}
100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
}
.grammar-global-notification {
position: fixed;
top: 20px;
right: 20px;
background: #2196F3;
color: white;
padding: 12px 16px;
border-radius: 8px;
z-index: 100000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
font-size: 14px;
max-width: 400px;
word-wrap: break-word;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.grammar-global-notification.show {
transform: translateX(0);
}
.grammar-global-notification.success {
background: #4CAF50;
}
.grammar-global-notification.error {
background: #f44336;
}
.grammar-global-notification.warning {
background: #FF9800;
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
// Inject settings panel styles
injectSettingsPanelStyles();
}
function injectSettingsPanelStyles() {
const settingsStyles = `
.grammar-settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 99998;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.grammar-settings-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
z-index: 99999;
width: 90%;
max-width: 700px;
max-height: 90vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
animation: scaleIn 0.3s ease forwards;
}
@keyframes scaleIn {
to {
transform: translate(-50%, -50%) scale(1);
}
}
.grammar-settings-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.grammar-settings-title h2 {
margin: 0;
font-size: 1.5em;
font-weight: 600;
}
.grammar-settings-subtitle {
opacity: 0.9;
font-size: 0.9em;
margin-top: 4px;
}
.grammar-settings-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.grammar-settings-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.grammar-settings-content {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
}
.settings-section {
margin-bottom: 24px;
animation: slideDown 0.4s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.settings-section h3 {
margin: 0 0 16px 0;
font-size: 1.1em;
color: #495057;
border-bottom: 1px solid #e9ecef;
padding-bottom: 8px;
}
.settings-row {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 8px 0;
}
.settings-row label {
flex: 1;
margin-right: 16px;
font-size: 14px;
color: #495057;
}
.settings-row input[type="text"],
.settings-row input[type="password"],
.settings-row input[type="number"],
.settings-row select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
min-width: 200px;
transition: border-color 0.2s ease;
}
.settings-row input:focus,
.settings-row select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.settings-row input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.1);
}
.shortcut-input {
background: #f8f9fa !important;
cursor: pointer;
min-width: 120px !important;
text-align: center;
font-family: monospace;
}
.shortcut-input.recording {
background: #fff3cd !important;
border-color: #ffc107;
color: #856404;
}
.grammar-settings-footer {
padding: 20px 24px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.grammar-settings-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.grammar-settings-btn.primary {
background: #667eea;
color: white;
}
.grammar-settings-btn.primary:hover {
background: #5a6fd8;
transform: translateY(-1px);
}
.grammar-settings-btn.secondary {
background: #6c757d;
color: white;
}
.grammar-settings-btn.secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
@media (max-width: 768px) {
.grammar-settings-panel {
width: 95%;
height: 95vh;
}
.settings-row {
flex-direction: column;
align-items: flex-start;
}
.settings-row label {
margin-bottom: 8px;
margin-right: 0;
}
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = settingsStyles;
document.head.appendChild(styleSheet);
}
// Initialize the script
async function init() {
try {
console.log('🔄 Starting Grammar Fixer initialization...');
await loadSettings();
applyDomainSpecificRules();
injectStyles();
setupGlobalShortcuts();
setupElementObservers();
registerMenuCommands();
console.log('✅ Smart Grammar Fixer Pro initialized successfully');
console.log('🌐 Current domain:', currentDomain);
showGlobalNotification('Grammar Fixer Pro ready! Use ' + settings.shortcuts.smartFix + ' to fix grammar.', 'success', 3000);
} catch (error) {
console.error('❌ Failed to initialize grammar fixer:', error);
showGlobalNotification('Grammar fixer failed to initialize', 'error');
}
}
function applyDomainSpecificRules() {
const domainRule = settings.domainRules[currentDomain];
if (domainRule) {
Object.assign(settings, domainRule);
if (settings.debugMode) {
console.log('🔧 Applied domain-specific rules for:', currentDomain, domainRule);
}
}
}
// Settings management
async function loadSettings() {
try {
const savedSettings = await GM_getValue('grammarSettings');
if (savedSettings) {
settings = Object.assign({}, DEFAULT_SETTINGS, savedSettings);
}
} catch (error) {
console.error('Error loading settings:', error);
settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
}
async function saveSettings() {
try {
await GM_setValue('grammarSettings', settings);
return true;
} catch (error) {
console.error('Error saving settings:', error);
showGlobalNotification('Failed to save settings', 'error');
return false;
}
}
// Menu commands
function registerMenuCommands() {
GM_registerMenuCommand('⚙️ Grammar Settings', showSettingsPanel);
GM_registerMenuCommand('🔄 Toggle Enabled', toggleEnabled);
GM_registerMenuCommand('📊 Status Info', showStatusInfo);
}
function toggleEnabled() {
settings.enabled = !settings.enabled;
saveSettings();
showGlobalNotification(`Grammar fixer ${settings.enabled ? 'enabled' : 'disabled'}`);
if (!settings.enabled) {
removeAllUIElements();
} else {
setTimeout(() => {
document.querySelectorAll('textarea, input[type="text"], [contenteditable="true"]')
.forEach(addSmartIconsToElement);
}, 1000);
}
}
function showStatusInfo() {
const status = `
Status: ${settings.enabled ? '✅ Enabled' : '❌ Disabled'}
Domain: ${currentDomain}
Main Language: ${settings.language.main}
Correction Language: ${settings.language.correctionLanguage}
Auto-detect: ${settings.language.autoDetect ? '✅' : '❌'}
Shortcuts: ${Object.values(settings.shortcuts).join(', ')}
`.trim();
showGlobalNotification(status, 'info', 5000);
}
// Shortcuts
function setupGlobalShortcuts() {
document.addEventListener('keydown', function(e) {
if (!settings.enabled) return;
const activeEl = document.activeElement;
if (!isTextElement(activeEl)) return;
// Smart Fix
if (checkShortcut(e, settings.shortcuts.smartFix)) {
e.preventDefault();
e.stopPropagation();
handleSmartGrammarFix(activeEl);
}
// Fix and Send
if (checkShortcut(e, settings.shortcuts.fixAndSend)) {
e.preventDefault();
e.stopPropagation();
handleFixAndSend(activeEl);
}
// Quick Fix
if (checkShortcut(e, settings.shortcuts.quickFix)) {
e.preventDefault();
e.stopPropagation();
handleQuickFix(activeEl);
}
// Toggle Enabled
if (checkShortcut(e, settings.shortcuts.toggleEnabled)) {
e.preventDefault();
e.stopPropagation();
toggleEnabled();
}
// Open Settings
if (checkShortcut(e, settings.shortcuts.openSettings)) {
e.preventDefault();
e.stopPropagation();
showSettingsPanel();
}
}, true);
}
function checkShortcut(event, shortcut) {
const keys = shortcut.split('+');
let match = true;
keys.forEach(key => {
key = key.trim().toLowerCase();
if (key === 'alt' && !event.altKey) match = false;
else if (key === 'ctrl' && !event.ctrlKey) match = false;
else if (key === 'shift' && !event.shiftKey) match = false;
else if (key === 'enter' && event.key !== 'Enter') match = false;
else if (key.length === 1 && event.key.toLowerCase() !== key) match = false;
else if (key.length > 1 && !['alt', 'ctrl', 'shift', 'enter'].includes(key)) {
// Handle special keys
if (key === 'space' && event.key !== ' ') match = false;
else if (event.key.toLowerCase() !== key) match = false;
}
});
return match;
}
function handleQuickFix(element) {
if (!settings.enabled || isProcessing) return;
const text = getElementText(element);
if (text.length < settings.correction.minTextLength) return;
fixWithLanguageTool(text, settings.language.main)
.then(fixedText => {
setElementText(element, fixedText);
showNotification(element, 'Quick fix applied', 'success');
})
.catch(error => {
console.error('Quick fix error:', error);
showNotification(element, 'Quick fix failed', 'error');
});
}
// UI Management
function setupElementObservers() {
if (!settings.enabled || !settings.ui.showIcons) return;
// Add to existing elements
setTimeout(() => {
document.querySelectorAll('textarea, input[type="text"], input[type="email"], input[type="search"], [contenteditable="true"]')
.forEach(addSmartIconsToElement);
}, 1000);
document.addEventListener('focusin', function(e) {
if (isTextElement(e.target)) {
addSmartIconsToElement(e.target);
}
});
}
function isTextElement(element) {
return element.tagName === 'TEXTAREA' ||
(element.tagName === 'INPUT' && (
element.type === 'text' ||
element.type === 'email' ||
element.type === 'search' ||
element.type === 'url' ||
!element.type
)) ||
element.isContentEditable;
}
function addSmartIconsToElement(element) {
if (element._grammarIconsAdded || !settings.ui.showIcons) return;
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const icon = document.createElement('div');
icon.className = 'grammar-helper-icon';
icon.innerHTML = 'A<div class="grammar-language-badge">LT</div>';
icon.title = `Fix Grammar (${settings.shortcuts.smartFix})`;
icon.addEventListener('click', async () => {
const text = getElementText(element);
if (text.trim()) {
await handleSmartGrammarFix(element);
}
});
positionIcon(icon, element);
document.body.appendChild(icon);
element._grammarIconsAdded = true;
}
function positionIcon(icon, element) {
const rect = element.getBoundingClientRect();
const scrollX = window.scrollX || window.pageXOffset;
const scrollY = window.scrollY || window.pageYOffset;
const top = rect.top + scrollY - 30;
const left = rect.right + scrollX - 30;
icon.style.top = top + 'px';
icon.style.left = left + 'px';
}
function removeAllUIElements() {
document.querySelectorAll('.grammar-helper-icon, .grammar-notification')
.forEach(el => el.remove());
document.querySelectorAll('[class*="grammar"]').forEach(el => {
if (el._grammarIconsAdded) {
el._grammarIconsAdded = false;
}
});
}
// Core Grammar Functions
async function handleSmartGrammarFix(element) {
if (isProcessing) {
showNotification(element, 'Already fixing grammar...', 'warning');
return;
}
isProcessing = true;
const text = getElementText(element);
if (!text.trim() || text.length < settings.correction.minTextLength) {
showNotification(element, 'Text too short to fix', 'warning');
isProcessing = false;
return;
}
if (text.length > settings.correction.maxTextLength) {
showNotification(element, 'Text too long to fix', 'warning');
isProcessing = false;
return;
}
try {
// Add processing animation to icon
const icon = element.parentNode?.querySelector('.grammar-helper-icon');
if (icon && settings.ui.animations) {
icon.classList.add('processing');
}
showNotification(element, 'Fixing grammar...', 'processing');
let languageCode = settings.language.correctionLanguage;
// Auto-detect language if set to auto
if (languageCode === 'auto') {
if (settings.language.autoDetect && settings.apiKeys.detectlanguage) {
try {
const detectedLang = await detectLanguage(text);
languageCode = detectedLang.code;
console.log(`🌍 Detected language: ${detectedLang.name} (${detectedLang.code})`);
} catch (error) {
console.warn('Language detection failed, using main language:', error);
languageCode = settings.language.main;
}
} else {
languageCode = settings.language.main;
}
}
const fixedText = await fixWithLanguageTool(text, languageCode);
setElementText(element, fixedText);
// Remove processing animation
if (icon && settings.ui.animations) {
icon.classList.remove('processing');
}
showNotification(element, `Grammar fixed! (${LANGUAGE_SUPPORT[languageCode] || languageCode})`, 'success');
} catch (error) {
console.error('Smart grammar fix error:', error);
// Remove processing animation on error
const icon = element.parentNode?.querySelector('.grammar-helper-icon');
if (icon && settings.ui.animations) {
icon.classList.remove('processing');
}
showNotification(element, 'Failed to fix grammar', 'error');
} finally {
isProcessing = false;
}
}
async function detectLanguage(text) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: API_CONFIG.detectlanguage,
headers: {
'Authorization': `Bearer ${settings.apiKeys.detectlanguage}`,
'Content-Type': 'application/json'
},
data: JSON.stringify({ q: text }),
timeout: 10000,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.data && data.data.detections && data.data.detections.length > 0) {
const detection = data.data.detections[0];
if (detection.confidence >= settings.language.confidenceThreshold) {
const langName = LANGUAGE_SUPPORT[detection.language] || detection.language;
resolve({
code: detection.language,
name: langName,
confidence: detection.confidence
});
}
}
reject('No confident language detection');
} catch (e) {
reject('Error parsing language detection response: ' + e);
}
},
onerror: reject,
ontimeout: () => reject('Language detection timeout')
});
});
}
async function fixWithLanguageTool(text, languageCode) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: API_CONFIG.languagetool,
data: `text=${encodeURIComponent(text)}&language=${languageCode}&enabledOnly=false`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 15000,
onload: function(response) {
try {
if (response.status !== 200) {
reject(`LanguageTool API error: ${response.status}`);
return;
}
const result = JSON.parse(response.responseText);
let fixedText = text;
if (result.matches && result.matches.length > 0) {
const matches = result.matches.sort((a, b) => b.offset - a.offset);
for (const match of matches) {
if (match.replacements && match.replacements.length > 0) {
const replacement = match.replacements[0].value;
const before = fixedText.substring(0, match.offset);
const after = fixedText.substring(match.offset + match.length);
fixedText = before + replacement + after;
}
}
}
resolve(fixedText);
} catch (e) {
reject('Error parsing LanguageTool response: ' + e);
}
},
onerror: reject,
ontimeout: () => reject('LanguageTool request timeout')
});
});
}
async function handleFixAndSend(element) {
await handleSmartGrammarFix(element);
if (settings.correction.autoFixOnSend) {
setTimeout(() => clickSendButton(element), 500);
}
}
function clickSendButton(element) {
const sendSelectors = [
'button[type="submit"]', 'input[type="submit"]',
'button[data-testid*="send"]', 'button[data-testid*="submit"]',
'[aria-label*="send" i]', '[aria-label*="submit" i]'
];
let sendButton = null;
const form = element.closest('form');
if (form) {
for (const selector of sendSelectors) {
sendButton = form.querySelector(selector);
if (sendButton && sendButton.offsetParent !== null) break;
}
}
if (sendButton) {
sendButton.click();
return true;
}
return false;
}
// Utility Functions
function getElementText(element) {
if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
return element.value;
} else {
return element.textContent || element.innerText || '';
}
}
function setElementText(element, text) {
if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
element.value = text;
} else {
element.textContent = text;
}
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
function showNotification(element, message, type = 'info') {
if (!settings.ui.showNotifications) return;
// Remove existing notification
const existingNotification = document.querySelector('.grammar-notification');
if (existingNotification) {
if (settings.ui.animations) {
existingNotification.classList.add('hide');
setTimeout(() => existingNotification.remove(), 300);
} else {
existingNotification.remove();
}
}
const notification = document.createElement('div');
notification.className = `grammar-notification ${type}`;
notification.textContent = message;
const rect = element.getBoundingClientRect();
const scrollX = window.scrollX || window.pageXOffset;
const scrollY = window.scrollY || window.pageYOffset;
notification.style.top = (rect.top + scrollY - 40) + 'px';
notification.style.left = (rect.right + scrollX - 10) + 'px';
document.body.appendChild(notification);
// Add show animation
if (settings.ui.animations) {
setTimeout(() => {
notification.classList.add('show');
}, 10);
// Auto hide after 3 seconds
setTimeout(() => {
notification.classList.add('hide');
setTimeout(() => {
if (notification.parentNode) notification.remove();
}, 300);
}, 3000);
} else {
// No animation - just remove after 3 seconds
setTimeout(() => {
if (notification.parentNode) notification.remove();
}, 3000);
}
}
function showGlobalNotification(message, type = 'info', duration = 3000) {
if (!settings.ui.showNotifications) return;
const notification = document.createElement('div');
notification.className = `grammar-global-notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
if (settings.ui.animations) {
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) notification.remove();
}, 300);
}, duration);
} else {
setTimeout(() => {
if (notification.parentNode) notification.remove();
}, duration);
}
}
// Settings Panel
function showSettingsPanel() {
const existingPanel = document.getElementById('grammar-settings-panel');
if (existingPanel) existingPanel.remove();
const existingOverlay = document.getElementById('grammar-settings-overlay');
if (existingOverlay) existingOverlay.remove();
const overlay = document.createElement('div');
overlay.id = 'grammar-settings-overlay';
overlay.className = 'grammar-settings-overlay';
const panel = document.createElement('div');
panel.id = 'grammar-settings-panel';
panel.className = 'grammar-settings-panel';
panel.innerHTML = `
<div class="grammar-settings-header">
<div class="grammar-settings-title">
<h2>⚙️ Grammar Fixer Settings</h2>
<div class="grammar-settings-subtitle">LanguageTool + DetectLanguage</div>
</div>
<button class="grammar-settings-close" id="settings-close">×</button>
</div>
<div class="grammar-settings-content">
<div class="settings-section">
<h3>Core Settings</h3>
<div class="settings-row">
<label>
<input type="checkbox" id="enabled" ${settings.enabled ? 'checked' : ''}>
Enable Grammar Fixer
</label>
</div>
<div class="settings-row">
<label>
<input type="checkbox" id="showIcons" ${settings.ui.showIcons ? 'checked' : ''}>
Show Helper Icons
</label>
</div>
<div class="settings-row">
<label>
<input type="checkbox" id="showNotifications" ${settings.ui.showNotifications ? 'checked' : ''}>
Show Notifications
</label>
</div>
<div class="settings-row">
<label>
<input type="checkbox" id="animations" ${settings.ui.animations ? 'checked' : ''}>
Enable Animations
</label>
</div>
</div>
<div class="settings-section">
<h3>Language Settings</h3>
<div class="settings-row">
<label>Main Language:</label>
<select id="mainLanguage">
${Object.entries(LANGUAGE_SUPPORT).map(([code, name]) =>
`<option value="${code}" ${settings.language.main === code ? 'selected' : ''}>
${name}
</option>`
).join('')}
</select>
</div>
<div class="settings-row">
<label>Correction Language:</label>
<select id="correctionLanguage">
<option value="auto" ${settings.language.correctionLanguage === 'auto' ? 'selected' : ''}>Auto-detect</option>
${Object.entries(LANGUAGE_SUPPORT).map(([code, name]) =>
`<option value="${code}" ${settings.language.correctionLanguage === code ? 'selected' : ''}>
${name}
</option>`
).join('')}
</select>
</div>
<div class="settings-row">
<label>
<input type="checkbox" id="autoDetect" ${settings.language.autoDetect ? 'checked' : ''}>
Auto-detect Language (for auto mode)
</label>
</div>
</div>
<div class="settings-section">
<h3>Keyboard Shortcuts</h3>
${Object.entries(settings.shortcuts).map(([action, shortcut]) => `
<div class="settings-row">
<label>${formatActionName(action)}:</label>
<input type="text" class="shortcut-input" id="shortcut-${action}" value="${shortcut}" readonly>
<button class="grammar-settings-btn secondary change-shortcut" data-action="${action}">Change</button>
</div>
`).join('')}
</div>
<div class="settings-section">
<h3>API Configuration</h3>
<div class="settings-row">
<label>DetectLanguage API Key:</label>
<input type="password" id="detectlanguageKey" value="${settings.apiKeys.detectlanguage}" placeholder="Enter your API key">
</div>
<small>Get free API key from detectlanguage.com</small>
</div>
<div class="settings-section">
<h3>Text Processing</h3>
<div class="settings-row">
<label>Min Text Length:</label>
<input type="number" id="minTextLength" value="${settings.correction.minTextLength}" min="1" max="100">
</div>
<div class="settings-row">
<label>
<input type="checkbox" id="autoFixOnSend" ${settings.correction.autoFixOnSend ? 'checked' : ''}>
Auto-fix on Send
</label>
</div>
</div>
</div>
<div class="grammar-settings-footer">
<button class="grammar-settings-btn secondary" id="settings-cancel">Cancel</button>
<button class="grammar-settings-btn primary" id="settings-save">Save Settings</button>
</div>
`;
// Event listeners
panel.querySelector('#settings-close').addEventListener('click', closeSettings);
panel.querySelector('#settings-cancel').addEventListener('click', closeSettings);
panel.querySelector('#settings-save').addEventListener('click', saveSettingsFromPanel);
// Shortcut recording
panel.querySelectorAll('.change-shortcut').forEach(button => {
button.addEventListener('click', function() {
const action = this.getAttribute('data-action');
const input = panel.querySelector(`#shortcut-${action}`);
startShortcutRecording(input, action);
});
});
overlay.addEventListener('click', closeSettings);
function closeSettings() {
if (settings.ui.animations) {
panel.style.animation = 'scaleIn 0.3s ease reverse';
overlay.style.animation = 'fadeIn 0.3s ease reverse';
setTimeout(() => {
panel.remove();
overlay.remove();
}, 300);
} else {
panel.remove();
overlay.remove();
}
}
function startShortcutRecording(input, action) {
input.value = 'Press new shortcut...';
input.classList.add('recording');
const handleKeyDown = (e) => {
e.preventDefault();
e.stopPropagation();
// Ignore modifier keys alone
if (['Alt', 'Control', 'Shift', 'Meta'].includes(e.key)) {
return;
}
let shortcut = '';
if (e.altKey) shortcut += 'Alt+';
if (e.ctrlKey) shortcut += 'Ctrl+';
if (e.shiftKey) shortcut += 'Shift+';
if (e.key === ' ') {
shortcut += 'Space';
} else if (e.key === 'Enter') {
shortcut += 'Enter';
} else if (e.key.length === 1) {
shortcut += e.key.toUpperCase();
} else {
shortcut += e.key;
}
input.value = shortcut;
input.classList.remove('recording');
document.removeEventListener('keydown', handleKeyDown);
};
document.addEventListener('keydown', handleKeyDown, { once: false });
// Cancel on escape
const handleEscape = (e) => {
if (e.key === 'Escape') {
input.value = settings.shortcuts[action];
input.classList.remove('recording');
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape, { once: true });
}
async function saveSettingsFromPanel() {
settings.enabled = panel.querySelector('#enabled').checked;
settings.ui.showIcons = panel.querySelector('#showIcons').checked;
settings.ui.showNotifications = panel.querySelector('#showNotifications').checked;
settings.ui.animations = panel.querySelector('#animations').checked;
settings.language.main = panel.querySelector('#mainLanguage').value;
settings.language.correctionLanguage = panel.querySelector('#correctionLanguage').value;
settings.language.autoDetect = panel.querySelector('#autoDetect').checked;
settings.apiKeys.detectlanguage = panel.querySelector('#detectlanguageKey').value;
settings.correction.minTextLength = parseInt(panel.querySelector('#minTextLength').value);
settings.correction.autoFixOnSend = panel.querySelector('#autoFixOnSend').checked;
// Save shortcuts
Object.keys(settings.shortcuts).forEach(action => {
const input = panel.querySelector(`#shortcut-${action}`);
if (input && input.value !== 'Press new shortcut...') {
settings.shortcuts[action] = input.value;
}
});
await saveSettings();
closeSettings();
showGlobalNotification('Settings saved successfully!', 'success');
}
document.body.appendChild(overlay);
document.body.appendChild(panel);
// Close on Escape
document.addEventListener('keydown', function closeOnEscape(e) {
if (e.key === 'Escape') {
closeSettings();
document.removeEventListener('keydown', closeOnEscape);
}
});
}
function formatActionName(action) {
return action.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Global access for debugging
unsafeWindow.grammarFixer = {
settings: settings,
fixText: handleSmartGrammarFix,
showSettings: showSettingsPanel
};
})();