Created
November 26, 2025 14:01
-
-
Save bmsimo/18bbb1493b969275f402056bd7ae775c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 103a10426c3efc7c275a93ee2a4102665ec61efd Mon Sep 17 00:00:00 2001 | |
| From: Mohamed Bermaki <simobermaki@gmail.com> | |
| Date: Wed, 26 Nov 2025 12:39:48 +0100 | |
| Subject: [PATCH] Feature: AI Rewriter | |
| --- | |
| app/browser/preload.js | 5 +- | |
| app/browser/tools/aiRewriter.js | 261 ++++++++++++++++++++++++++++++++ | |
| app/index.js | 94 +++++++++++- | |
| app/security/ipcValidator.js | 3 +- | |
| 4 files changed, 352 insertions(+), 11 deletions(-) | |
| create mode 100644 app/browser/tools/aiRewriter.js | |
| diff --git a/app/browser/preload.js b/app/browser/preload.js | |
| index e2ddeea..ae55150 100644 | |
| --- a/app/browser/preload.js | |
| +++ b/app/browser/preload.js | |
| @@ -346,7 +346,8 @@ document.addEventListener('DOMContentLoaded', async () => { | |
| { name: "trayIconRenderer", path: "./tools/trayIconRenderer" }, | |
| { name: "mqttStatusMonitor", path: "./tools/mqttStatusMonitor" }, | |
| { name: "disableAutogain", path: "./tools/disableAutogain" }, | |
| - { name: "navigationButtons", path: "./tools/navigationButtons" } | |
| + { name: "navigationButtons", path: "./tools/navigationButtons" }, | |
| + { name: "aiRewriter", path: "./tools/aiRewriter" } | |
| ]; | |
| let successCount = 0; | |
| @@ -354,7 +355,7 @@ document.addEventListener('DOMContentLoaded', async () => { | |
| try { | |
| const moduleInstance = require(module.path); | |
| // CRITICAL: mqttStatusMonitor needs ipcRenderer for IPC communication (see CLAUDE.md) | |
| - if (module.name === "settings" || module.name === "theme" || module.name === "trayIconRenderer" || module.name === "mqttStatusMonitor") { | |
| + if (module.name === "settings" || module.name === "theme" || module.name === "trayIconRenderer" || module.name === "mqttStatusMonitor" || module.name === "aiRewriter") { | |
| moduleInstance.init(config, ipcRenderer); | |
| } else { | |
| moduleInstance.init(config); | |
| diff --git a/app/browser/tools/aiRewriter.js b/app/browser/tools/aiRewriter.js | |
| new file mode 100644 | |
| index 0000000..819b618 | |
| --- /dev/null | |
| +++ b/app/browser/tools/aiRewriter.js | |
| @@ -0,0 +1,261 @@ | |
| +// app/browser/tools/aiRewriter.js | |
| +let config = null; | |
| +let ipcRenderer = null; | |
| + | |
| +function init(cfg, ipc) { | |
| + config = cfg; | |
| + ipcRenderer = ipc; | |
| + | |
| + // Default to enabled unless explicitly disabled | |
| + if (config.aiRewriterEnabled === false) return; | |
| + | |
| + // console.log('[AI Rewriter] Initializing...'); | |
| + setupRewriteButton(); | |
| +} | |
| + | |
| +function setupRewriteButton() { | |
| + // console.log('[AI Rewriter] Setting up MutationObserver for composer detection...'); | |
| + | |
| + // Use MutationObserver to wait for Teams UI | |
| + const observer = new MutationObserver(() => { | |
| + checkAndInjectButton(); | |
| + }); | |
| + | |
| + observer.observe(document.body, { childList: true, subtree: true }); | |
| + | |
| + // Also do initial checks after delays (Teams loads progressively) | |
| + [2000, 5000, 10000].forEach(delay => { | |
| + setTimeout(() => { | |
| + // console.log(`[AI Rewriter] Running composer check after ${delay}ms...`); | |
| + checkAndInjectButton(); | |
| + }, delay); | |
| + }); | |
| +} | |
| + | |
| +function checkAndInjectButton() { | |
| + const composer = document.querySelector('[data-tid="ckeditor-replyConversation"]') | |
| + || document.querySelector('[data-tid="messageTextArea"]') | |
| + || document.querySelector('[data-track-module-id="compose"]') | |
| + || document.querySelector('[contenteditable="true"][role="textbox"]') | |
| + || document.querySelector('.cke_editable'); | |
| + | |
| + if (composer && !document.querySelector('#ai-rewrite-btn')) { | |
| + // console.log('[AI Rewriter] Found composer element:', composer.className || composer.tagName); | |
| + injectRewriteButton(composer); | |
| + } else if (!composer) { | |
| + // console.log('[AI Rewriter] Composer not found yet'); | |
| + } | |
| +} | |
| + | |
| +function injectRewriteButton(composer) { | |
| + // console.log('[AI Rewriter] Injecting button...'); | |
| + | |
| + const btn = document.createElement('button'); | |
| + btn.id = 'ai-rewrite-btn'; | |
| + btn.textContent = 'AI Rewrite'; | |
| + btn.style.cssText = ` | |
| + margin: 4px; | |
| + padding: 6px 12px; | |
| + cursor: pointer; | |
| + background: #5b5fc7; | |
| + color: white; | |
| + border: none; | |
| + border-radius: 4px; | |
| + font-size: 12px; | |
| + font-weight: 500; | |
| + z-index: 9999; | |
| + position: relative; | |
| + `; | |
| + | |
| + btn.addEventListener('click', async () => { | |
| + const text = composer.innerText || composer.textContent; | |
| + // console.log('[AI Rewriter] Button clicked, text:', text?.substring(0, 100)); | |
| + | |
| + if (!text.trim()) { | |
| + // console.log('[AI Rewriter] Text is empty, aborting'); | |
| + return; | |
| + } | |
| + | |
| + btn.disabled = true; | |
| + btn.textContent = 'β³ Rewriting...'; | |
| + | |
| + try { | |
| + // console.log('[AI Rewriter] Calling IPC ai-rewrite-text...'); | |
| + // Use IPC to route through main process | |
| + const rewritten = await ipcRenderer.invoke('ai-rewrite-text', { text }); | |
| + // console.log('[AI Rewriter] IPC returned:', rewritten?.substring(0, 100)); | |
| + | |
| + // Show popup with corrected text | |
| + showRewritePopup(rewritten, composer); | |
| + } catch (err) { | |
| + console.error('[AI Rewriter] IPC call failed:', err); | |
| + alert('AI Rewrite failed: ' + err.message); | |
| + } finally { | |
| + btn.disabled = false; | |
| + btn.textContent = 'β¨ AI Rewrite'; | |
| + } | |
| + }); | |
| + | |
| + // Try to insert button near the composer | |
| + try { | |
| + const parent = composer.parentElement; | |
| + if (parent) { | |
| + parent.insertBefore(btn, composer); | |
| + // console.log('[AI Rewriter] Button inserted before composer'); | |
| + } else { | |
| + // Fallback: append to body as fixed button | |
| + btn.style.position = 'fixed'; | |
| + btn.style.bottom = '80px'; | |
| + btn.style.right = '20px'; | |
| + document.body.appendChild(btn); | |
| + // console.log('[AI Rewriter] Button appended to body (fixed position)'); | |
| + } | |
| + } catch (err) { | |
| + console.error('[AI Rewriter] Failed to insert button:', err); | |
| + // Last resort: fixed position button | |
| + btn.style.position = 'fixed'; | |
| + btn.style.bottom = '80px'; | |
| + btn.style.right = '20px'; | |
| + document.body.appendChild(btn); | |
| + // console.log('[AI Rewriter] Button appended to body as fallback'); | |
| + } | |
| +} | |
| + | |
| +function showRewritePopup(text, composer) { | |
| + // Remove existing popup if any | |
| + const existingPopup = document.querySelector('#ai-rewrite-popup'); | |
| + if (existingPopup) existingPopup.remove(); | |
| + | |
| + // Create popup container | |
| + const popup = document.createElement('div'); | |
| + popup.id = 'ai-rewrite-popup'; | |
| + popup.style.cssText = ` | |
| + position: fixed; | |
| + bottom: 100px; | |
| + right: 20px; | |
| + width: 350px; | |
| + max-width: 90vw; | |
| + background: #292929; | |
| + border: 1px solid #444; | |
| + border-radius: 8px; | |
| + box-shadow: 0 4px 20px rgba(0,0,0,0.4); | |
| + z-index: 99999; | |
| + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| + color: #fff; | |
| + `; | |
| + | |
| + // Header | |
| + const header = document.createElement('div'); | |
| + header.style.cssText = ` | |
| + padding: 12px 16px; | |
| + border-bottom: 1px solid #444; | |
| + display: flex; | |
| + justify-content: space-between; | |
| + align-items: center; | |
| + font-weight: 600; | |
| + font-size: 14px; | |
| + `; | |
| + header.textContent = 'β¨ AI Corrected Text'; | |
| + | |
| + // Close button | |
| + const closeBtn = document.createElement('button'); | |
| + closeBtn.textContent = 'β'; | |
| + closeBtn.style.cssText = ` | |
| + background: none; | |
| + border: none; | |
| + color: #999; | |
| + font-size: 18px; | |
| + cursor: pointer; | |
| + padding: 0; | |
| + line-height: 1; | |
| + `; | |
| + closeBtn.onclick = () => popup.remove(); | |
| + header.appendChild(closeBtn); | |
| + | |
| + // Text content | |
| + const content = document.createElement('div'); | |
| + content.style.cssText = ` | |
| + padding: 16px; | |
| + font-size: 14px; | |
| + line-height: 1.5; | |
| + white-space: pre-wrap; | |
| + word-break: break-word; | |
| + max-height: 200px; | |
| + overflow-y: auto; | |
| + `; | |
| + content.textContent = text; | |
| + | |
| + // Button container | |
| + const buttons = document.createElement('div'); | |
| + buttons.style.cssText = ` | |
| + padding: 12px 16px; | |
| + border-top: 1px solid #444; | |
| + display: flex; | |
| + gap: 8px; | |
| + `; | |
| + | |
| + // Copy button | |
| + const copyBtn = document.createElement('button'); | |
| + copyBtn.textContent = 'Copy'; | |
| + copyBtn.style.cssText = ` | |
| + flex: 1; | |
| + padding: 8px 16px; | |
| + background: #5b5fc7; | |
| + color: white; | |
| + border: none; | |
| + border-radius: 4px; | |
| + cursor: pointer; | |
| + font-size: 13px; | |
| + font-weight: 500; | |
| + `; | |
| + copyBtn.onclick = async () => { | |
| + try { | |
| + await navigator.clipboard.writeText(text); | |
| + copyBtn.textContent = 'β Copied!'; | |
| + setTimeout(() => { | |
| + copyBtn.textContent = 'Copy'; | |
| + }, 2000); | |
| + } catch (err) { | |
| + console.error('[AI Rewriter] Copy failed:', err); | |
| + copyBtn.textContent = 'Failed'; | |
| + } | |
| + }; | |
| + | |
| + // Copy & Close button | |
| + const copyCloseBtn = document.createElement('button'); | |
| + copyCloseBtn.textContent = 'Copy & Close'; | |
| + copyCloseBtn.style.cssText = ` | |
| + flex: 1; | |
| + padding: 8px 16px; | |
| + background: #4a4a4a; | |
| + color: white; | |
| + border: none; | |
| + border-radius: 4px; | |
| + cursor: pointer; | |
| + font-size: 13px; | |
| + font-weight: 500; | |
| + `; | |
| + copyCloseBtn.onclick = async () => { | |
| + try { | |
| + await navigator.clipboard.writeText(text); | |
| + popup.remove(); | |
| + // Focus back on composer so user can paste | |
| + composer.focus(); | |
| + } catch (err) { | |
| + console.error('[AI Rewriter] Copy failed:', err); | |
| + } | |
| + }; | |
| + | |
| + buttons.appendChild(copyBtn); | |
| + buttons.appendChild(copyCloseBtn); | |
| + | |
| + popup.appendChild(header); | |
| + popup.appendChild(content); | |
| + popup.appendChild(buttons); | |
| + | |
| + document.body.appendChild(popup); | |
| + | |
| + // console.log('[AI Rewriter] Popup displayed'); | |
| +} | |
| + | |
| +module.exports = { init }; | |
| \ No newline at end of file | |
| diff --git a/app/index.js b/app/index.js | |
| index 737727e..58f339d 100644 | |
| --- a/app/index.js | |
| +++ b/app/index.js | |
| @@ -11,7 +11,10 @@ const CustomBackground = require("./customBackground"); | |
| const { MQTTClient } = require("./mqtt"); | |
| const GraphApiClient = require("./graphApi"); | |
| const { registerGraphApiHandlers } = require("./graphApi/ipcHandlers"); | |
| -const { validateIpcChannel, allowedChannels } = require("./security/ipcValidator"); | |
| +const { | |
| + validateIpcChannel, | |
| + allowedChannels, | |
| +} = require("./security/ipcValidator"); | |
| const globalShortcuts = require("./globalShortcuts"); | |
| const CommandLineManager = require("./startup/commandLine"); | |
| const NotificationService = require("./notifications/service"); | |
| @@ -21,6 +24,7 @@ const PartitionsManager = require("./partitions/manager"); | |
| const IdleMonitor = require("./idle/monitor"); | |
| const os = require("node:os"); | |
| const isMac = os.platform() === "darwin"; | |
| +const http = require('node:http'); | |
| // Support for E2E testing: use temporary userData directory for clean state | |
| if (process.env.E2E_USER_DATA_DIR) { | |
| @@ -82,7 +86,10 @@ const partitionsManager = new PartitionsManager(appConfig.settingsStore); | |
| const idleMonitor = new IdleMonitor(config, getUserStatus); | |
| // Initialize custom notification manager for toast notifications | |
| -const customNotificationManager = new CustomNotificationManager(config, mainAppWindow); | |
| +const customNotificationManager = new CustomNotificationManager( | |
| + config, | |
| + mainAppWindow | |
| +); | |
| if (isMac) { | |
| requestMediaAccess(); | |
| @@ -117,8 +124,12 @@ if (gotTheLock) { | |
| ipcMain.handle = (channel, handler) => { | |
| return originalIpcHandle(channel, (event, ...args) => { | |
| if (!validateIpcChannel(channel, args.length > 0 ? args[0] : null)) { | |
| - console.error(`[IPC Security] Rejected handle request for channel: ${channel}`); | |
| - return Promise.reject(new Error(`Unauthorized IPC channel: ${channel}`)); | |
| + console.error( | |
| + `[IPC Security] Rejected handle request for channel: ${channel}` | |
| + ); | |
| + return Promise.reject( | |
| + new Error(`Unauthorized IPC channel: ${channel}`) | |
| + ); | |
| } | |
| return handler(event, ...args); | |
| }); | |
| @@ -191,6 +202,65 @@ if (gotTheLock) { | |
| canGoForward: webContents?.navigationHistory?.canGoForward() || false, | |
| }; | |
| }); | |
| + | |
| + ipcMain.handle("ai-rewrite-text", async (_event, { text }) => { | |
| + console.log("[AI Rewriter] === IPC Handler Called ==="); | |
| + console.log("[AI Rewriter] Received text:", text?.substring(0, 100)); | |
| + | |
| + return new Promise((resolve, reject) => { | |
| + const postData = JSON.stringify({ | |
| + model: "gemma3:4b", | |
| + prompt: text, | |
| + system: `You are a professional text editor. Your task is to correct and improve the following French text. | |
| + - Fix any spelling, grammar, and punctuation errors | |
| + - Make the text clearer but not too formal | |
| + - Keep the original meaning and tone | |
| + - Only return the corrected text, nothing else (no explanations, no quotes).`, | |
| + stream: false | |
| + }); | |
| + | |
| + console.log("[AI Rewriter] Sending to Ollama:", postData.substring(0, 200)); | |
| + | |
| + const req = http.request( | |
| + { | |
| + hostname: "127.0.0.1", | |
| + port: 11434, | |
| + path: "/api/generate", | |
| + method: "POST", | |
| + headers: { | |
| + "Content-Type": "application/json", | |
| + "Content-Length": Buffer.byteLength(postData), | |
| + }, | |
| + }, | |
| + (res) => { | |
| + console.log("[AI Rewriter] Ollama response status:", res.statusCode); | |
| + let data = ""; | |
| + res.on("data", (chunk) => (data += chunk)); | |
| + res.on("end", () => { | |
| + console.log("[AI Rewriter] Ollama raw response:", data.substring(0, 500)); | |
| + try { | |
| + const parsed = JSON.parse(data); | |
| + console.log("[AI Rewriter] Parsed response field:", parsed.response?.substring(0, 200)); | |
| + const result = parsed.response?.trim() || text; | |
| + console.log("[AI Rewriter] Returning:", result.substring(0, 100)); | |
| + resolve(result); | |
| + } catch (e) { | |
| + console.error("[AI Rewriter] Failed to parse Ollama response:", e); | |
| + console.error("[AI Rewriter] Raw data was:", data); | |
| + reject(e); | |
| + } | |
| + }); | |
| + } | |
| + ); | |
| + | |
| + req.on("error", (err) => { | |
| + console.error("[AI Rewriter] Ollama request failed:", err.message); | |
| + reject(err); | |
| + }); | |
| + req.write(postData); | |
| + req.end(); | |
| + }); | |
| + }); | |
| } else { | |
| console.info("App already running"); | |
| app.quit(); | |
| @@ -283,7 +353,11 @@ async function handleAppReady() { | |
| mqttClient.initialize(); | |
| } | |
| - await mainAppWindow.onAppReady(appConfig, new CustomBackground(app, config), screenSharingService); | |
| + await mainAppWindow.onAppReady( | |
| + appConfig, | |
| + new CustomBackground(app, config), | |
| + screenSharingService | |
| + ); | |
| // Initialize Graph API client if enabled (after mainAppWindow is ready) | |
| if (config.graphApi?.enabled) { | |
| @@ -291,9 +365,13 @@ async function handleAppReady() { | |
| const mainWindow = mainAppWindow.getWindow(); | |
| if (mainWindow) { | |
| graphApiClient.initialize(mainWindow); | |
| - console.debug("[GRAPH_API] Graph API client initialized with main window"); | |
| + console.debug( | |
| + "[GRAPH_API] Graph API client initialized with main window" | |
| + ); | |
| } else { | |
| - console.warn("[GRAPH_API] Main window not available, Graph API client not fully initialized"); | |
| + console.warn( | |
| + "[GRAPH_API] Main window not available, Graph API client not fully initialized" | |
| + ); | |
| } | |
| } | |
| @@ -304,7 +382,7 @@ async function handleAppReady() { | |
| globalShortcuts.register(config, mainAppWindow, app); | |
| // Log IPC Security configuration status | |
| - console.log('π IPC Security: Channel allowlisting enabled'); | |
| + console.log("π IPC Security: Channel allowlisting enabled"); | |
| console.log(`π IPC Security: ${allowedChannels.size} channels allowlisted`); | |
| } | |
| diff --git a/app/security/ipcValidator.js b/app/security/ipcValidator.js | |
| index a3ceaf9..0916977 100644 | |
| --- a/app/security/ipcValidator.js | |
| +++ b/app/security/ipcValidator.js | |
| @@ -68,7 +68,8 @@ const allowedChannels = new Set([ | |
| 'graph-api-get-calendar-events', | |
| 'graph-api-get-calendar-view', | |
| 'graph-api-create-calendar-event', | |
| - 'graph-api-get-mail-messages' | |
| + 'graph-api-get-mail-messages', | |
| + 'ai-rewrite-text' | |
| ]); | |
| /** | |
| -- | |
| 2.43.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment