Skip to content

Instantly share code, notes, and snippets.

@bmsimo
Created November 26, 2025 14:01
Show Gist options
  • Select an option

  • Save bmsimo/18bbb1493b969275f402056bd7ae775c to your computer and use it in GitHub Desktop.

Select an option

Save bmsimo/18bbb1493b969275f402056bd7ae775c to your computer and use it in GitHub Desktop.
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