├── .roo └── mcp.json ├── .cursorignore ├── extension ├── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon48.png │ ├── icon16.js │ ├── icon48.js │ ├── icon128.js │ └── generate_icons.html ├── fontawesome │ └── webfonts │ │ ├── fa-brands-400.ttf │ │ ├── fa-solid-900.ttf │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-regular-400.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 ├── manifest.json ├── panel-inline.js ├── panel-inline.css ├── bridge.js ├── panel.html ├── background.js ├── panel.css ├── content.js └── panel.js ├── backend ├── src │ ├── types │ │ └── qrcode-terminal.d.ts │ ├── index.ts │ ├── whatsapp.service.ts │ └── ai.service.ts ├── tsconfig.json ├── package.json └── install.js ├── .gitignore └── README.md /.roo/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": {} 3 | } -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | -------------------------------------------------------------------------------- /extension/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/icons/icon128.png -------------------------------------------------------------------------------- /extension/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/icons/icon16.png -------------------------------------------------------------------------------- /extension/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/icons/icon48.png -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /extension/fontawesome/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romiluz13/whatsapp_ai/HEAD/extension/fontawesome/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /backend/src/types/qrcode-terminal.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'qrcode-terminal' { 2 | const qrcodeTerminal: { 3 | generate: (text: string, options?: { small?: boolean }) => void 4 | }; 5 | export default qrcodeTerminal; 6 | } -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true, 12 | "typeRoots": ["./node_modules/@types", "./src/types"] 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } -------------------------------------------------------------------------------- /extension/icons/icon16.js: -------------------------------------------------------------------------------- 1 | // This is a helper file to create a base64-encoded icon for the extension 2 | // You can replace these files with actual icons later 3 | 4 | // 16x16 icon - Base64 encoded PNG with WhatsApp green background and white robot icon 5 | const icon16Base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAABFUlEQVQ4jcWTvUpDQRCFv7nZdRNCELQRgoWFhU3AzlJ8Ah9AfARrsRCfQbDTF7CwESxs0qSzkA2IYKHgD8k9FtkbN5sbaGPjwMAwO+d8Z2ZnYc7KigJu9G4Ft24Lna6h08YE4tnjL4HbnCju7wRb36N96wEEoZ8oKnf3Hlu+l/V3BUc3DzUBUr+f+Jkxwq0ucH+E7QWTYmLkGRiZSXGmDN6b6HRLNYvfYIU7GWy9ilDRYCNqrxkIm7AeQWUF9qsgBQ6OAb/KYO8CXj9Sqg9S6p0wvNyGYj6V6QzkBFvLcHmbMJrMgH9i8PIerO+AcwmsmCEYw9MrbE1LGZUB+PgC0UlOFbOrsHsGzpWn8L9fJ8Dc9QWYqLYdnrWyYgAAAABJRU5ErkJggg=="; 6 | 7 | // You can use this data URI in your extension 8 | console.log('Icon 16x16:', 'data:image/png;base64,' + icon16Base64); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | package-lock.json # Usually committed, but can be ignored if preferred by project 7 | lerna-debug.log* 8 | 9 | # Environment variables 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | .history 16 | 17 | # IDEs and editors 18 | .idea/ 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | *.sublime-workspace 25 | 26 | # macOS 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Windows 32 | Thumbs.db 33 | ehthumbs.db 34 | Desktop.ini 35 | 36 | # Project Specific 37 | memory-bank/ 38 | backend/.env 39 | backend/.wwebjs_auth/ 40 | 41 | # Build output (if any in the future) 42 | dist/ 43 | build/ 44 | out/ 45 | 46 | # Optional: Chrome extension packaging files (if generated locally) 47 | *.crx 48 | *.pem 49 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-ai-copilot-backend", 3 | "version": "1.0.0", 4 | "description": "Backend for WhatsApp AI Co-Pilot Chrome Extension", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "ts-node src/index.ts", 8 | "build": "tsc", 9 | "dev": "nodemon --exec ts-node src/index.ts", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "setup": "node install.js" 12 | }, 13 | "keywords": [ 14 | "whatsapp", 15 | "ai", 16 | "copilot", 17 | "backend" 18 | ], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "cors": "^2.8.5", 23 | "dotenv": "^16.3.1", 24 | "express": "^4.18.2", 25 | "openai": "^4.12.1", 26 | "qrcode-terminal": "^0.12.0", 27 | "whatsapp-web.js": "^1.23.0" 28 | }, 29 | "devDependencies": { 30 | "@types/cors": "^2.8.17", 31 | "@types/express": "^4.17.17", 32 | "@types/node": "^20.6.2", 33 | "nodemon": "^3.0.1", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^5.2.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "WhatsApp AI Co-Pilot", 4 | "version": "0.1.0", 5 | "description": "AI assistant for WhatsApp Web, built with your awesome plan!", 6 | "permissions": [ 7 | "storage", 8 | "activeTab", 9 | "alarms", 10 | "scripting" 11 | ], 12 | "host_permissions": [ 13 | "https://web.whatsapp.com/*", 14 | "http://localhost:3000/*" 15 | ], 16 | "content_security_policy": { 17 | "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* http://127.0.0.1:*;" 18 | }, 19 | "background": { 20 | "service_worker": "background.js" 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": ["https://web.whatsapp.com/*"], 25 | "js": ["content.js"], 26 | "css": ["panel.css"] 27 | } 28 | ], 29 | "web_accessible_resources": [ 30 | { 31 | "resources": [ 32 | "panel.html", "panel.css", "panel.js", 33 | "fontawesome/css/all.min.css", 34 | "fontawesome/webfonts/*" 35 | ], 36 | "matches": ["https://web.whatsapp.com/*"] 37 | } 38 | ], 39 | "action": { 40 | "default_title": "WhatsApp AI Co-Pilot" 41 | }, 42 | "icons": { 43 | "16": "icons/icon16.png", 44 | "48": "icons/icon48.png", 45 | "128": "icons/icon128.png" 46 | } 47 | } -------------------------------------------------------------------------------- /extension/icons/icon48.js: -------------------------------------------------------------------------------- 1 | // This is a helper file to create a base64-encoded icon for the extension 2 | // You can replace these files with actual icons later 3 | 4 | // 48x48 icon - Base64 encoded PNG with WhatsApp green background and white robot icon 5 | const icon48Base64 = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEuklEQVR4nO2Z3W8UVRTGf3dm2i1LXUoLtFS+GwgVCkJCAlFAPhJEo8YYEzXxgyiJiTHGxPgFxhg/okajJsYQEvUP0BgVBYxUQA20ha8WSrfQj9nd7cfM9cVzb8+d2ZnpbXenlvgkJzN37j3nPPfcc8499w6swAIscRhzqVPsOIgdBTJqDAAFoMzKgvp9ZpxuFN1WUK8XJxbp4tbrdKoJCkDEgCQQxY0DFJOxGCuLEchidBwMnGrn0FEDBtQNKWRnFlkl4vY6iiKwDdioaBuQQhrXi1MF+lGIIdtF3JByQcjKwzVk9WPqO9OI4yb6Ox9gK3Bdfe5D8YFLaA9QMgQpVZCBooZ2I7adQHrqmkfxJXdRvMD0IaFUVL9LwB3N9r1onoGvgD3AYaRdHJHddSFkObm5W7MNm5h2CQz8CnwD/AL8BfTWaqAWAmYULtAOQkZCymJ9G4GoyF0yGpH2oiJwT/1fZPbC/TwCCVwV8tOI5iMwiqzGjDqnLbcYsQIMi9kHyniRrAWBEuIsI+qnHeZPwQzSrh/JYBBNnXDw2KnTgcnCUmhGYvkWxeeIQAV4GUkZS4pqUaigJlNIjJbwRyOhZVIz9sUIo5GcwhVFZVzuK0UCQCF2iXExSCuBNO4uDBKO27jGJWQ5dYRVz4bXuFJBAqKI8gVmE7FxucbCyWWugTDUcqCfkEzaI6PmEDuPbOTzCHg9lkHxEjPiINKhDMI9Xm3jcJUNXtLz1YA3hCJI200FtU+xMXTRBJrxwBSiY6sO1ELAVXlIdl2YZgJFZEda0LwnrXbMdoqQauTniUBBB4PZF9YLtYhq1UeXiKYAo2L0vRVtZZ6wCYxrdi+wB9lMa8FYRZL4BvA40Ilknr2+smMIy08h4dCK5gJx5Ll1vS4cU7QWZMXeAU4hGS5sTCBV+rvAVeAykhmDYSFk2ZsRR24htMUQtQ/qNzDZBMAzwMeaXVhjbSPEnsWf/RZEARHqrSFUDVPSa4rWjtQKHcAnDXQyCnRr9muI7YNg2zjMJjCJCNSGqLFQHOjS7LPIjhxGPWl0TXhTaTfCEdmJJmQnwqAmgV2a3YnUDj7UG0bVCNjA35odRXbCQlYxLJpJInWBjn5kxX3IoFbKXAvmkONaDJjfH0EIFBHP25p9GdnmQSRRDhHsMbPVxjRY6Syk+cERQKDWMfENpAx5TdGagBNAV8DYKeBnzT6GbOc4LqGKRsZEQSPxWyPxW+tK2cgbIwUtvq0hGvAm8CEyCz1IHO+ttsDKwEOIp/cg4VPGWyTZfnIVJHmFeSw9huwRDf8A7yBPUxHkRWWIYA9MIfH6EfC+Gm8hp0ELf7EV5H1HfwbeBL5HahIHKZTPAo8ghV4ESVvbgE7kyTWNhFY38BvyCvkxcuQyC7lqpCzL9u621cSZl5D8fwGYwDgIQZ6eo8jDZgLx2INI/NtIcdaMHCsLyAnwOnKGNxchUNQcTKuTnw3cRapivzrm4mEfcvn+I7Jze5Gd6WH+b+W6kRe9S8BHBGRcHTZSJM0gKjKlxuJ5ahpFPUciAWPMa/bHShisFLCCJYL/AWG+GZZj6TFuAAAAAElFTkSuQmCC"; 6 | 7 | // You can use this data URI in your extension 8 | console.log('Icon 48x48:', 'data:image/png;base64,' + icon48Base64); -------------------------------------------------------------------------------- /extension/panel-inline.js: -------------------------------------------------------------------------------- 1 | // Icon fallback script 2 | document.addEventListener('DOMContentLoaded', function() { 3 | // Check if Font Awesome is loaded properly 4 | const isFontAwesomeLoaded = Array.from(document.styleSheets) 5 | .some(sheet => sheet.href && sheet.href.includes('font-awesome')); 6 | 7 | if (!isFontAwesomeLoaded) { 8 | console.log('Font Awesome not loaded, using fallback icons'); 9 | // Add text fallbacks for each icon 10 | document.querySelectorAll('.card-icon').forEach(icon => { 11 | const iconType = icon.querySelector('i')?.className || ''; 12 | let fallbackText = ''; 13 | 14 | if (iconType.includes('reply')) fallbackText = 'SR'; 15 | else if (iconType.includes('chart')) fallbackText = 'AC'; 16 | else if (iconType.includes('bell')) fallbackText = 'RM'; 17 | else if (iconType.includes('clipboard')) fallbackText = 'DD'; 18 | else fallbackText = 'AI'; 19 | 20 | icon.innerHTML = `${fallbackText}`; 21 | }); 22 | 23 | // Replace other icons with text 24 | document.querySelectorAll('.logo-icon i').forEach(icon => { 25 | icon.parentNode.innerHTML = 'AI'; 26 | }); 27 | 28 | document.querySelectorAll('.section-title i').forEach(icon => { 29 | icon.outerHTML = '📝'; 30 | }); 31 | 32 | document.querySelectorAll('.send-button i').forEach(icon => { 33 | icon.outerHTML = '→'; 34 | }); 35 | 36 | document.querySelectorAll('.fold-button i').forEach(icon => { 37 | icon.outerHTML = '←'; 38 | }); 39 | 40 | document.querySelectorAll('.toggle-icon i').forEach(icon => { 41 | icon.outerHTML = '→'; 42 | }); 43 | 44 | document.querySelectorAll('.action-button i').forEach((icon, index) => { 45 | const symbols = ['💡', '⚙️', 'ℹ️']; 46 | icon.outerHTML = symbols[index] || '•'; 47 | }); 48 | } 49 | 50 | // Add click handlers 51 | document.querySelectorAll('.feature-card').forEach(card => { 52 | card.addEventListener('click', function() { 53 | // Add visual active feedback 54 | document.querySelectorAll('.feature-card').forEach(c => 55 | c.classList.remove('active')); 56 | this.classList.add('active'); 57 | }); 58 | }); 59 | }); -------------------------------------------------------------------------------- /backend/install.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require('readline'); 3 | const path = require('path'); 4 | 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }); 9 | 10 | console.log("\n===== WhatsApp AI Co-Pilot Setup =====\n"); 11 | console.log("This script will help you configure your OpenAI API key.\n"); 12 | 13 | // Check for existing .env file 14 | const envPath = path.join(__dirname, '.env'); 15 | let existingKey = ''; 16 | 17 | if (fs.existsSync(envPath)) { 18 | try { 19 | const envContent = fs.readFileSync(envPath, 'utf8'); 20 | const match = envContent.match(/OPENAI_API_KEY=([^\s]*)/); 21 | 22 | if (match && match[1] && match[1] !== 'your_openai_api_key_here') { 23 | existingKey = match[1]; 24 | console.log(`An existing OpenAI API key was found (${existingKey.slice(0, 3)}...${existingKey.slice(-4)}).`); 25 | } 26 | } catch (err) { 27 | console.error("Error reading .env file:", err.message); 28 | } 29 | } 30 | 31 | const askApiKey = () => { 32 | const defaultPrompt = existingKey ? 33 | `Enter your OpenAI API key (press Enter to keep existing key): ` : 34 | `Enter your OpenAI API key: `; 35 | 36 | rl.question(defaultPrompt, (apiKey) => { 37 | const keyToUse = apiKey.trim() || existingKey; 38 | 39 | if (!keyToUse) { 40 | console.log("Error: No API key provided. The key is required for AI functionality."); 41 | return askApiKey(); 42 | } 43 | 44 | // Ask for model preference 45 | rl.question(`Select AI model (default is gpt-4o-mini):\n1. gpt-4o-mini (faster, cheaper)\n2. gpt-4o (more powerful)\nEnter choice [1/2]: `, (modelChoice) => { 46 | let model = 'gpt-4o-mini'; // Default 47 | 48 | if (modelChoice.trim() === '2') { 49 | model = 'gpt-4o'; 50 | } 51 | 52 | // Create or update .env file 53 | const envContent = `# WhatsApp AI Co-Pilot Environment Variables 54 | 55 | # OpenAI API Key 56 | OPENAI_API_KEY=${keyToUse} 57 | 58 | # Server port 59 | PORT=3000 60 | 61 | # OpenAI Model to use 62 | OPENAI_MODEL=${model} 63 | `; 64 | 65 | try { 66 | fs.writeFileSync(envPath, envContent); 67 | console.log("\n✅ Configuration saved successfully!"); 68 | console.log("\nYou can now start the backend with:"); 69 | console.log(" npm start"); 70 | console.log("\nAnd install the Chrome extension from the extension/ folder."); 71 | 72 | } catch (err) { 73 | console.error("Error writing .env file:", err.message); 74 | } 75 | 76 | rl.close(); 77 | }); 78 | }); 79 | }; 80 | 81 | askApiKey(); -------------------------------------------------------------------------------- /extension/panel-inline.css: -------------------------------------------------------------------------------- 1 | /* Base styling for the panel */ 2 | :root { 3 | --primary-color: #128C7E; 4 | --secondary-color: #34B7F1; 5 | --text-color: #333; 6 | --background-color: #f8f9fa; 7 | --sidebar-width: 320px; 8 | --sidebar-collapsed-width: 10px; 9 | --header-height: 50px; 10 | --animation-duration: 0.3s; 11 | } 12 | 13 | /* Reset styles */ 14 | * { 15 | margin: 0; 16 | padding: 0; 17 | box-sizing: border-box; 18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 19 | } 20 | 21 | /* Panel container */ 22 | #ai-copilot-panel { 23 | width: 100%; 24 | height: 100vh; 25 | display: flex; 26 | flex-direction: column; 27 | background-color: var(--background-color); 28 | color: var(--text-color); 29 | overflow: hidden; 30 | } 31 | 32 | /* Panel header */ 33 | .panel-header { 34 | display: flex; 35 | justify-content: space-between; 36 | align-items: center; 37 | padding: 0 15px; 38 | height: var(--header-height); 39 | background-color: var(--primary-color); 40 | color: white; 41 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 42 | } 43 | 44 | .panel-title { 45 | display: flex; 46 | align-items: center; 47 | gap: 10px; 48 | font-weight: bold; 49 | font-size: 18px; 50 | } 51 | 52 | .fold-button { 53 | background: none; 54 | border: none; 55 | color: white; 56 | cursor: pointer; 57 | width: 30px; 58 | height: 30px; 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | border-radius: 50%; 63 | transition: background-color 0.2s; 64 | } 65 | 66 | .fold-button:hover { 67 | background-color: rgba(255, 255, 255, 0.1); 68 | } 69 | 70 | /* Panel content */ 71 | .panel-content { 72 | flex: 1; 73 | overflow-y: auto; 74 | padding: 15px; 75 | } 76 | 77 | /* Feature grid */ 78 | .feature-grid { 79 | display: grid; 80 | grid-template-columns: repeat(2, 1fr); 81 | gap: 10px; 82 | margin-bottom: 20px; 83 | } 84 | 85 | /* Message styles */ 86 | .message { 87 | margin-bottom: 10px; 88 | padding: 8px 12px; 89 | border-radius: 10px; 90 | max-width: 85%; 91 | animation: fadeIn 0.3s; 92 | } 93 | 94 | .message.user { 95 | background-color: #dcf8c6; 96 | align-self: flex-end; 97 | margin-left: auto; 98 | } 99 | 100 | .message.assistant { 101 | background-color: #f3f3f3; 102 | align-self: flex-start; 103 | margin-right: auto; 104 | } 105 | 106 | .message.system { 107 | background-color: #e2f1fb; 108 | align-self: center; 109 | margin: 10px auto; 110 | font-style: italic; 111 | text-align: center; 112 | } 113 | 114 | .message-content { 115 | word-break: break-word; 116 | } 117 | 118 | /* Toggle button for collapsed sidebar */ 119 | #sidebar-toggle { 120 | position: fixed; 121 | left: 0; 122 | top: 50%; 123 | transform: translateY(-50%); 124 | background-color: var(--primary-color); 125 | color: white; 126 | padding: 10px 5px; 127 | border-radius: 0 5px 5px 0; 128 | cursor: pointer; 129 | box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); 130 | transition: opacity 0.3s, transform 0.3s; 131 | } 132 | 133 | #sidebar-toggle.hidden { 134 | display: none; 135 | } 136 | 137 | /* Animation classes */ 138 | @keyframes fadeIn { 139 | from { opacity: 0; transform: translateY(10px); } 140 | to { opacity: 1; transform: translateY(0); } 141 | } 142 | 143 | /* Loading indicators */ 144 | .loading-dots { 145 | display: inline-flex; 146 | gap: 3px; 147 | } 148 | 149 | .loading-dots span { 150 | width: 8px; 151 | height: 8px; 152 | background-color: var(--text-color); 153 | border-radius: 50%; 154 | animation: dotPulse 1.5s infinite ease-in-out; 155 | } 156 | 157 | .loading-dots span:nth-child(2) { 158 | animation-delay: 0.2s; 159 | } 160 | 161 | .loading-dots span:nth-child(3) { 162 | animation-delay: 0.4s; 163 | } 164 | 165 | @keyframes dotPulse { 166 | 0%, 100% { transform: scale(0.6); opacity: 0.6; } 167 | 50% { transform: scale(1); opacity: 1; } 168 | } -------------------------------------------------------------------------------- /extension/icons/icon128.js: -------------------------------------------------------------------------------- 1 | // This is a helper file to create a base64-encoded icon for the extension 2 | // You can replace these files with actual icons later 3 | 4 | // 128x128 icon - Base64 encoded PNG with WhatsApp green background and white robot icon 5 | const icon128Base64 = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAK+0lEQVR4nO2d+48dVRnHv2fm3Lt7l0K7tNBLbwK90AIthV6AXqAUCoXKTZCSEpUqoRgJUROjUfxFE+PvJppoYkgav/CDJv4gCihSQJCUQrnsFlro/d5295w/4Jm8Z86Zc+bMmZm9fJOTbXfmzLzzvM/7vM/zPDMDVKhQoUKFChUqVKhQoUKFChUqVChQ4aSBZa1go9mAZrOOLMXIE+FbQSNiWIPYRQQjAPbQez0AKtliCsAogEkAh+j3FIBJDOzcnrWSdQDo1lCza9C89mSRsmWB7CaAPQD2A3gfwG76zP9sAOy9JGkHFT5e6CC7pwE8DOAZAOuoo34SRO9s3Xy0gfOvBm44A3j3ReCNh4DLHgQOPpO2lw0AGwE8Qx1gCNrqL1UgQM/N3Ac8OADcNQa8cR8w/hTQvyltSwbVQ08DWAlgGYCz6LNWABgmWzgA3dKUJLKDCtliGXlbCmC1ZAvpW0l2MQnN1Ty2s9lsDXhM6X65hgaA81dBt4a1NwGPbgK+cw7wwlXAmvOBq4aAvbemamwGwCsAXgHwFoCt9PcIfd5Ldutdb9J7vQH35mlkAxUCkALOA3ALgCcBHCF7eAm4f1nSxowB3AbgewBOL6XHK4jBVA/Y9BSwj+T/dwB+CWAVgMEkjQwAuA3Az+jsf7Rk+a8wmXc68Nwq4L1XgRdXA1cO5m7xdAA/BbAawOcrxndwAf2DMo8cBa4ZQSayGP2+AcDdAJ4DMAidUCpkiF7KoUQJgK0/A84aQWbiBH8A+CaA+wBUyl9AED9wGtl1JgJAZTb0wH8B+CmAMwrr4QoGZJxo3gbgnBztnkEe7DMVB5AvujLqmhnACgB3AeiL2aYJ4DW67nkewLf0z/sBvAs9k+dhAIeJy+iftdYaKnhYcTlw3gR9VXbP8wC+m7ARJWgB0CqgLJDX7ZNOB/DwnMvG+8AuOteJx3ga+vr2fwCe17y+DcBvobmCFwAc0CwL86uZmrN/rPH+EwfwHQA3pu+ZE4vHrgQefRgaF6oHgeL8XTMCV9CRlwXEDrKDuQS2jQC+Qh0WhS30+jmAPwF4m/4eA3CM3nNpMwD0KH/L1/vWs/Nqk/0utzlJv3HbktvztqGs571w0P2Ov34A3A2gD9q68OT/9QBeB/Axzf1iLg+A5jICZehlN3vNfVrOMFrk+x5Q9I2LhujtZmUbs/RbPiHqcm/6PfXIv3NpJ9l2RbrE9+YToNuuIpYfIidIJbNWV5nBDmoAdgC4gM7gKlw9D9gpAD9A6/wud1yGc9yiQdMgBQNEkZLHUMv3tQEgVzB7yVPG5V/XLmLaCGPd+PUg2a2Ly0gR4ym1wvNIWYOaM9YVVDvmGCbXXF3pZFiSBZIXCv4sEFP+OB+b6JwDDVZrHw1U1SzAAwCbAeyj9+2C5XQxTQN+mJ6fhY8fOAzgfyh//tdMbb5N44oAi897qJqg8weagWn2xFX3wjiwbwdpgDjUgkXlLgC/AnCNb2nRCOvIH/wuFXp8hIKCZW5KcJ0v+hSIaTtPiuYsP5ADCNMA43Uq+wB8A8CJHAMxrPZbJtxFZoqTwdHSRZ7MIpKmUhSEmmk9cyBb5RlnAuR2Y/TuUdKMYRjCOBN0LYpEgcyEjX9Kw7LsIlFESBhPoJJDXRuCo6I7JpmnZwCMAXAVqnNbCfvhXWYyHQ4vA3CLgEfvH4NWE0zTqvJGG1tT85PDk9gRF3ZnZJUMPkjzfF1BnPMOPXR0e9hL0+9hANfQ2XkjTWABwH0ADgdcoxsEV8kANJsOtrR40LqHLiM/2eFI8OPSTYsbSF3HIZM9TDj0/m7S+gXUdnTcm7cQqYOwyoA+2pnGEmw/hIB1FNGxuIHUcRJQtEsHcCFw16hbBUw0vQNiKiUn8n0PQPCYDtFY7aYxWlICsG8bmUIGPIDoReAadpVaUTlqOWsAxkhnkKF5N3H3E8AfALxBs+kR6Ug6lQgBDPk4tOJQleNIkC78R5oBGIfbfYT0FfQ5aPV4rOMfAHARNDdRLB6Jwi5KVf4W7pQd2eCl+SJ5xSMBRWyAlXUjC4grfVMUdtU1Wm8C8BdohdSRbZsoDkEuQmn65lHoBZgcMYwTgVvk+wC84LlWLQJZRXlwMZ2JReCsePldAE8BmAjZn9YFpVL+RRdO5JFRfGZodZRaV3EtAK2lCWpHdX0OXUF5/uLZDiKCxkIHiJ2+DsCnATwN4BnohLAsFAG7WktTCxlhJlTIaJgArJrlDjADiBnCHbgwALvoBhSPCDWgGUR+aDZlKWTB08zHyLkvPsdblAXEr22L/xf7IyYvHwRYt01YzSDaiUTzB2dMXZwPKKf4uPH2fDnzrHH5vgWANcCcvRIBM6m1QGqBizgA0QAuTwE3Aq+dA/zvduD8lUHlG8R3HAxrA3PPcgAMo6IFwCLwkfMX9SZCzGcQi5lrO1qiiFzv0Zcsh2mAgKWqo9j9pXOAPRsBABz4M4YOAnNbvSJ0AOdgxzJ5EgpwmGKimqrZHpgOLkYD+0j+Lzzm6Rv7cWIUWDQGH2ZVx4EZKk/DWAHQbOCcAjRe0YsqVDnYzQJG5lmYhhDpfIBpCDZP7D/VQCZ7NvUhJFHEMIDuoxaUC4CrgFhzzO+5j4FLB9GC3dxPnx/AKRkK7mL6FoEVkxQTl6nfFkzDvPxM12wPJ1lc/eAqgK2AAy6/rYNuHFE1QZZXDGQAZQxVBSc4+7d8BrhrmAbDnqH3Rlf4dXaP8MQcvWtxAOdoMSoamgCYGbGQgasICxs0mRV0Fs0HNFPegLI2sLbw4OXA/ftgwEZ1PbVaQOPYS5w+DeCZS9TDKyK3mNdA9ZEuAbCdRVMNQBfZnrQNrSwWp14bAIrqp5wq/udNwOVDMGB3n36ZvqY9gThIQsRRMEkAP9EwbBJojPmPWQTu+g+XOC/gxgNkNmBfAn0+eSbw1E4YsOcUePq4hVn9yVBOTjRDXgTCM3SX6TLmPNQWiOUGstYCJgHksO3cEiHQdQVNQNO3rQBcPQQDdmb/6Ur9DsmjDyBcjJ/MJNi7NLpmqBUEMxUdFNhVzhIBWsC/zcgGlOvjqUXgjX/GbWguwDsYXMG2K9tTAbfR4oHm6BEPsEiQkRoDw7h95uIFM9YGpocbVDkLWbQs4GxI3EJTaAbRjj5ZT9pQ9n5RZAUBOr/TlbCnMHWn/OwmgJn9xDN8VaFXRx5wCkDK9l2CwGLkZQUBdwPy+8jlCMp1gGv1L/wDI4sAZiJLnS6dDl3qLa/PVRe+aA0g1wZZ/KsLszOK8LTjACbVq7Kia8/3/ZnGgSnvgBSL/1/ENzLiEsCJIFM4QlsZsKCBTFuQYpvTEVjMdpgMFFqo7SWMfx8J+wJkGNvIXAJVo1+I2yrBpaxskc9uBJuXIGkTqgGy1gLyJFBxkp9jqwS4zZjbJlTUAWYoG8hmKFv7x9DYt3ACeGoYUbB4BLJ9NdgbA2Nt4FXYcofVT8pqFshXC9hPBGVuQRk7yvwxvjO03lujrfjj/7yqiKLJ5jHLt0A86eUW8+OXV0PkCRqx6+mOtbZwbdPeMmCrKNxDXM4K2p97UXECsBYMIoGXDGjYJCfxbQDPbpq/zGH7Zow+AHM6jjHOAXYnTTF0n1LWfhQGKIIQklHEPRHoOgkCPBEuQulE0dWD6oKc+Tyk9y11lY+OG0CTNIFqJxW/TUf3GC0SoChSKI4TYOu/7TtSN5V91LwYDcCdsFXIiCt/WGRQxQOUgDA+wF1bWJ3BHUAAHXcrVxmw1OiYUaRQ97yDFUbZ+5VPBFWkUOeRKVwgEijHiJ/rGi0Qm09UzgBsCNMAQaQQKG7nUoUSURQplCdZfQKoOIH4GMsRGsQHlOYGLsCT/MQEn21M1RO9Iw8gUMkgdclXZcIWmQK5U8H9iazaP0nxyaABlpMmMGXCVihQA3C/AXnqtwJ0+5+pVVigWEAD4GTAJymCuEKFChUqVKhQoUKFChUqVKhQoUKFBYT/A2/h6UqNSS2BAAAAAElFTkSuQmCC"; 6 | 7 | // You can use this data URI in your extension 8 | console.log('Icon 128x128:', 'data:image/png;base64,' + icon128Base64); -------------------------------------------------------------------------------- /extension/bridge.js: -------------------------------------------------------------------------------- 1 | // Bridge for messaging between the panel script and content script 2 | window.postMessageToExtension = function(message) { 3 | try { 4 | window.postMessage({ 5 | source: 'panel-script', 6 | message: message 7 | }, '*'); 8 | } catch (error) { 9 | console.error('Error in postMessageToExtension:', error); 10 | } 11 | }; 12 | 13 | // Override chrome.runtime.sendMessage in the page context 14 | window.chromeRuntimeSendMessage = function(message, callback) { 15 | try { 16 | // Generate a random ID for this message to track the response 17 | const messageId = 'msg_' + Math.random().toString(36).substr(2, 9); 18 | 19 | // Store the callback 20 | window.pendingCallbacks = window.pendingCallbacks || {}; 21 | window.pendingCallbacks[messageId] = callback; 22 | 23 | // Post the message to the content script with the ID 24 | window.postMessage({ 25 | source: 'panel-script', 26 | message: { 27 | ...message, 28 | _messageId: messageId 29 | } 30 | }, '*'); 31 | 32 | // Listen for the response 33 | const responseHandler = function(event) { 34 | try { 35 | if (event.source !== window) return; 36 | if (event.data.source === 'content-script' && 37 | event.data.destination === 'panel' && 38 | event.data.messageId === messageId) { 39 | // Execute the callback with the response 40 | if (typeof window.pendingCallbacks[messageId] === 'function') { 41 | try { 42 | window.pendingCallbacks[messageId](event.data.response); 43 | } catch (callbackError) { 44 | console.error('Error executing callback:', callbackError); 45 | } 46 | delete window.pendingCallbacks[messageId]; 47 | } 48 | // Remove this specific response handler 49 | window.removeEventListener('message', responseHandler); 50 | } 51 | } catch (handlerError) { 52 | console.error('Error in message response handler:', handlerError); 53 | window.removeEventListener('message', responseHandler); 54 | } 55 | }; 56 | 57 | window.addEventListener('message', responseHandler); 58 | 59 | // Set a timeout to clean up if no response is received 60 | setTimeout(() => { 61 | if (window.pendingCallbacks[messageId]) { 62 | delete window.pendingCallbacks[messageId]; 63 | window.removeEventListener('message', responseHandler); 64 | if (typeof callback === 'function') { 65 | try { 66 | callback({ error: 'Timeout waiting for response' }); 67 | } catch (timeoutError) { 68 | console.error('Error executing timeout callback:', timeoutError); 69 | } 70 | } 71 | } 72 | }, 10000); // 10 second timeout 73 | } catch (error) { 74 | console.error('Error in chromeRuntimeSendMessage:', error); 75 | if (typeof callback === 'function') { 76 | try { 77 | callback({ error: 'Error sending message: ' + error.message }); 78 | } catch (callbackError) { 79 | console.error('Error executing error callback:', callbackError); 80 | } 81 | } 82 | } 83 | }; 84 | 85 | // Safely override event handlers to prevent interference with WhatsApp 86 | document.addEventListener('DOMContentLoaded', function() { 87 | // Create a global error handler for our extension to avoid affecting WhatsApp 88 | window.addEventListener('error', function(event) { 89 | // Only handle errors coming from our extension files 90 | if (event.filename && ( 91 | event.filename.includes('panel.js') || 92 | event.filename.includes('bridge.js') || 93 | event.filename.includes('content.js') 94 | )) { 95 | console.error('WhatsApp AI Extension error:', event.error); 96 | event.preventDefault(); 97 | event.stopPropagation(); 98 | return true; // Prevent default error handler 99 | } 100 | }, true); 101 | }); -------------------------------------------------------------------------------- /extension/icons/generate_icons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Icon Generator 7 | 40 | 41 | 42 |

WhatsApp AI Co-Pilot Icon Generator

43 |

Click on each canvas to download the icon image

44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 | 154 | 155 | -------------------------------------------------------------------------------- /extension/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WhatsApp AI 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | WhatsApp AI 16 |
17 |
18 | 21 | 24 |
25 |
26 | 27 | 28 | 56 | 57 | 58 |
59 |
60 |

מאמת חיבור...

61 | 65 |
66 | 67 | 73 | 74 | 75 | 78 | 79 | 80 | 86 | 87 | 88 | 99 | 100 | 103 |
104 | 105 | 106 |
107 |
108 | 109 |
110 |
111 | 112 | 113 |
114 | 115 | 119 | 120 |
121 | 122 | 128 | 129 |
130 | 131 | 132 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 WhatsApp Group Insights AI 🤖 2 | 3 | **Tired of catching up on hundreds of WhatsApp messages? Unlock AI-powered summarization and Q&A directly within WhatsApp Web!** 🤯 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 7 | [![Node.js](https://img.shields.io/badge/Node.js->=18.x-blue.svg)](https://nodejs.org/) 8 | [![TypeScript](https://img.shields.io/badge/TypeScript-blue.svg)](https://www.typescriptlang.org/) 9 | 10 | --- 11 | 12 | # 🔥 **AI-Powered Setup: Your README is the Key!** 🔥 13 | 14 | **Got an AI Coding Assistant? (Cursor, Copilot, etc.)** 15 | 16 | Skip the manual steps! Just **copy this entire README.md file** and paste it into your AI assistant. 17 | Tell it: *"Set up this project for me."* 18 | 19 | Your AI will handle the cloning, installations, and configurations following the detailed instructions below. Welcome to 2025! 😉 20 | 21 | --- 22 | 23 | WhatsApp Group Insights AI is an open-source Chrome extension that brings the power of Large Language Models (LLMs) like `gpt-4o-mini` directly to your WhatsApp Web interface. Get quick, intelligent summaries of long group discussions and ask specific questions about the content, saving you time and keeping you in the loop. 24 | 25 | This project has been refactored for a modern, intuitive user experience and a clean, maintainable codebase, making it ready for community contributions and further innovation! 26 | 27 | --- 28 | 29 | 30 | 37 | 38 | --- 39 | 40 | ## ✨ Core Features & The Big Idea 41 | 42 | This extension seamlessly integrates with your WhatsApp Web, offering: 43 | 44 | * **🤖 AI-Powered Summaries:** Instantly get the gist of long group chats. No more endless scrolling! 45 | * **❓ Intelligent Q&A:** Ask specific questions about any group conversation and get answers based on the actual message content. 46 | * **🔒 Secure Connection:** Connects to your WhatsApp account safely and lists your groups right in the extension. 47 | 48 | **The core idea?** To tame information overload in WhatsApp groups. We believe AI can make keeping up with busy chats effortless and efficient, giving you back valuable time. This is just the beginning, and we envision a future with even more intelligent group management tools. 49 | 50 | ## 💡 Why This Project? 51 | 52 | In today's fast-paced digital communication, WhatsApp groups can become overwhelming. This tool was born from a desire to: 53 | 54 | * **Save You Time:** Quickly grasp what's important without reading every single message. 55 | * **Boost Your Productivity:** Find information and answers within your group chats in seconds. 56 | * **Keep You In The Loop:** Effortlessly stay updated on crucial discussions. 57 | * **Pioneer with AI:** Showcase a practical way to embed powerful LLMs into everyday communication tools. 58 | 59 | We're building this in the open because we believe in the power of community. Your insights and contributions can help shape this tool into something truly amazing! 60 | 61 | ## 🛠️ Tech Stack 62 | 63 | * **Frontend (Extension Panel):** HTML, CSS, Vanilla JavaScript 64 | * **Backend Server:** Node.js, Express.js, TypeScript 65 | * **WhatsApp Integration:** `whatsapp-web.js` 66 | * **AI Integration:** OpenAI API (`gpt-4o-mini` or configurable) 67 | * **Browser:** Google Chrome (as an extension) 68 | 69 | ## 📂 Project Structure 70 | 71 | ``` 72 | whatsapp-ai-insights/ 73 | ├── backend/ # Node.js backend server 74 | │ ├── src/ 75 | │ │ ├── ai.service.ts # Handles OpenAI API interactions 76 | │ │ ├── index.ts # Express server setup and API routes 77 | │ │ └── whatsapp.service.ts # Manages whatsapp-web.js client 78 | │ ├── .env.example # Example environment file 79 | │ ├── package.json 80 | │ └── tsconfig.json 81 | ├── extension/ # Chrome extension files 82 | │ ├── assets/ # (Optional: for static assets) 83 | │ ├── fontawesome/ # FontAwesome library 84 | │ ├── icons/ # Extension icons (16, 48, 128) 85 | │ ├── panel.html # UI for the extension sidebar 86 | │ ├── panel.css # Styles for panel.html 87 | │ ├── panel.js # Logic for panel UI and communication 88 | │ ├── content.js # Injects sidebar, interacts with WhatsApp Web DOM 89 | │ ├── background.js # Handles background tasks, communication hub 90 | │ ├── bridge.js # Facilitates communication between panel and content script 91 | │ └── manifest.json # Extension manifest 92 | ├── .gitignore 93 | ├── LICENSE # Project License (e.g., MIT) 94 | ├── README.md # This file 95 | └── package.json # Root package.json (if any) 96 | ``` 97 | 98 | ## 🚀 Setup and Installation 99 | 100 | Getting started is easy! Follow these steps: 101 | 102 | ### Prerequisites 103 | 104 | * 🟢 Node.js (v18.x or later recommended) 105 | * 📦 npm or yarn 106 | * 🌐 Google Chrome browser 107 | * 🔑 An OpenAI API key with access to a model like `gpt-4o-mini` (or your preferred model). 108 | 109 | ### 1. Backend Setup ⚙️ 110 | 111 | 1. **Clone the Repository:** 112 | ```bash 113 | git clone https://github.com/romiluz13/whatsapp_ai.git 114 | cd whatsapp_ai 115 | ``` 116 | 2. **Navigate to Backend:** 117 | ```bash 118 | cd backend 119 | ``` 120 | 3. **Install Dependencies:** 121 | ```bash 122 | npm install 123 | # or 124 | # yarn install 125 | ``` 126 | 4. **Set Your OpenAI API Key (Choose ONE method):** 127 | * 🔑 **EASIEST METHOD (Recommended): Use the Extension Panel!** 128 | 1. After setting up the backend and loading the extension (see steps below), open WhatsApp Web. 129 | 2. Click the extension icon, then the **settings icon (⚙️)** within the panel. 130 | 3. Enter your OpenAI API key in the designated field and click "Save API Key". 131 | 4. *That's it! This key is stored locally in your browser and will be used for all AI requests.* 132 | * ⚙️ **Alternative Method (.env file for backend):** 133 | 1. Create a `.env` file in the `backend` directory. 134 | 2. Add your key: `OPENAI_API_KEY=your_openai_api_key_here` 135 | 3. You can also set `OPENAI_MODEL` (defaults to `gpt-4o-mini`) and `PORT` (defaults to `3000`) here. 136 | 4. *Note: If an API key is set in the extension panel, it will **override** the one in the `.env` file.* 137 | 5. **Start the Backend Server:** 138 | ```bash 139 | npm run dev 140 | # or 141 | # yarn dev 142 | ``` 143 | This will compile TypeScript and start the server (usually on `http://localhost:3000`). Look for a confirmation message in your terminal. 144 | 145 | ### 2. Chrome Extension Setup 🧩 146 | 147 | 1. Open Google Chrome and navigate to `chrome://extensions`. 148 | 2. Enable **Developer mode** (toggle switch, usually in the top right). 149 | 3. Click the **"Load unpacked"** button. 150 | 4. Select the `extension` folder from this project directory (`whatsapp_ai/extension`). 151 | 5. The "WhatsApp AI Insights" extension will appear in your list and be active. You should see its icon in your Chrome toolbar. 152 | 153 | ## 🎮 Usage 154 | 155 | 1. ✅ Ensure the backend server is running (from Backend Setup step 5). 156 | 2. 💬 Open WhatsApp Web (`web.whatsapp.com`) in your Chrome browser. 157 | 3. 📊 The WhatsApp AI Insights panel should automatically appear on the right side. 158 | 4. **Connect to WhatsApp:** 159 | * The panel will guide you. If needed, scan the QR code displayed in the panel (or at `http://localhost:3000/auth/qr-page` for a larger view) using the WhatsApp app on your phone. 160 | * Status will update to "Client is ready and connected to WhatsApp." 161 | 5. **Select a Group:** 162 | * Choose a group from the dropdown list. 163 | 6. **Summarize Group:** 164 | * Click "סכם קבוצה" (Summarize Group). An AI-generated summary of recent messages will appear. 165 | 7. **Ask a Question:** 166 | * Type your question about the selected group's conversation into the text area. 167 | * Click the send button (paper airplane icon). Get an AI-generated answer based on the conversation context. 168 | 169 | ## 🧑‍💻 Development 170 | 171 | ### Key Scripts (Backend) 172 | 173 | * `npm run build` or `yarn build`: Compile TypeScript to JavaScript (e.g., to a `dist` folder). 174 | * `npm start` or `yarn start`: Run the compiled JavaScript server (for production). 175 | * `npm run dev` or `yarn dev`: Run the server in development mode with auto-reloading. 176 | 177 | ## 🤝 Let's Build Together! 178 | 179 | Contributions are the lifeblood of open-source and are **highly encouraged!** Whether you're a seasoned developer or just starting, your ideas and code can make a huge impact. 180 | 181 | **Here's how you can jump in:** 182 | 183 | 1. **Fork** the repository on GitHub – make it your own playground! 184 | 2. Create a **new branch** for your brilliant feature or crucial bug fix (e.g., `feature/next-gen-summary` or `bugfix/chat-sync-error`). 185 | 3. Craft your changes and **commit** them with clear, descriptive messages – tell us your story! 186 | 4. **Push** your innovations to your forked repository. 187 | 5. Open a **Pull Request** to the main repository. Share your work, explain your changes, and let's discuss how to make this project even better! 188 | 189 | We value clean code, innovative ideas, and a collaborative spirit. Clone the repo, play around, and let's improve it together! 190 | 191 | ## 🤔 What's Next? 192 | 193 | While the core features are robust, here are some areas for potential future exploration by the community: 194 | 195 | * Even more nuanced user controls for summary length/style (beyond prompt augmentation). 196 | * Advanced caching strategies for further performance and API cost optimization. 197 | * Support for other LLM providers or local models. 198 | * Your great ideas! 199 | 200 | ## ❤️ Show Your Support 201 | 202 | If you find this project useful or interesting: 203 | * ⭐ Star the repository on GitHub! 204 | * 🔗 Share it on LinkedIn, Twitter, or with your friends and colleagues. 205 | * 💡 Suggest new features or report bugs by opening an issue. 206 | 207 | ## 📜 License 208 | 209 | This project is licensed under the MIT License. See the [`LICENSE`](LICENSE) file for details. 210 | *(Note: You'll need to add a LICENSE file to the repository, e.g., by creating one with the MIT License text.)* 211 | 212 | --- 213 | 214 | *Disclaimer: This tool interacts with WhatsApp Web. Ensure your use complies with WhatsApp's terms of service. The developers are not responsible for any misuse or account issues.* -------------------------------------------------------------------------------- /extension/background.js: -------------------------------------------------------------------------------- 1 | // WhatsApp AI Co-Pilot Background Service Worker 2 | console.log('Background service worker started.'); 3 | 4 | // --- Backend Interaction --- 5 | // The backend server at http://localhost:3000 is responsible for whatsapp-web.js client management. 6 | // This background script acts as a client to that backend and a message bridge. 7 | 8 | let backendGroups = []; // Stores groups fetched from the backend server 9 | let backendClientReady = false; // Status of the backend's WhatsApp client 10 | let lastBackendStatus = { ready: false, message: "Initializing...", qrCodeAvailable: false }; 11 | let panelReady = false; // To track if panel is ready to receive proactive updates 12 | let lastChatName = null; // Variable to be removed if CHAT_FOCUS_CHANGED is removed 13 | 14 | const BACKEND_URL = 'http://localhost:3000'; 15 | 16 | /** 17 | * Fetches the WhatsApp client status from the backend server. 18 | * Updates local state (`backendClientReady`, `lastBackendStatus`) and proactively 19 | * sends status updates to ready panel instances. 20 | * @returns {Promise} The status object from the backend or a default error status. 21 | */ 22 | async function fetchWhatsAppStatusFromBackend() { 23 | try { 24 | const response = await fetch(`${BACKEND_URL}/auth/status`); 25 | if (!response.ok) { 26 | let errorMsg = `Backend status check failed: ${response.status}`; 27 | try { 28 | const errorData = await response.json(); 29 | errorMsg = errorData.error || errorData.message || errorMsg; 30 | } catch (e) { /* ignore parsing error */ } 31 | throw new Error(errorMsg); 32 | } 33 | const status = await response.json(); // Expected: { ready: boolean, message: string, qrCodeAvailable: boolean } 34 | console.log('[BACKGROUND.JS] Fetched status from backend (/auth/status):', status); 35 | 36 | backendClientReady = status.ready || false; 37 | lastBackendStatus = status; 38 | 39 | // Proactive updates to panel (panel also polls, so these are supplementary) 40 | if (panelReady) { 41 | if (status.qrCodeAvailable && !status.ready) { 42 | sendMessageToAllTabs({ type: 'WHATSAPP_QR_AVAILABLE' }); // Inform panel QR is available for it to fetch 43 | } else if (status.ready) { 44 | sendMessageToAllTabs({ type: 'WHATSAPP_CLIENT_READY' }); // Inform panel client is ready for it to fetch groups 45 | } else { 46 | sendMessageToAllTabs({ type: 'WHATSAPP_STATUS_UPDATE', status }); 47 | } 48 | } 49 | return status; 50 | } catch (error) { 51 | console.error('[BACKGROUND.JS] Error fetching WhatsApp status from backend:', error); 52 | backendClientReady = false; 53 | lastBackendStatus = { ready: false, message: error.message, qrCodeAvailable: false }; 54 | if (panelReady) { 55 | sendMessageToAllTabs({ type: 'WHATSAPP_BACKEND_ERROR', error: error.message }); 56 | } 57 | return lastBackendStatus; 58 | } 59 | } 60 | 61 | /** 62 | * Fetches the list of WhatsApp groups from the backend server. 63 | * Caches the groups locally in `backendGroups` and `chrome.storage.local`. 64 | * Optionally sends a proactive update to the panel (though panel primarily fetches itself). 65 | * @param {boolean} [proactiveUpdate=false] - Whether to attempt a proactive update to the panel. 66 | * @returns {Promise>} An array of group objects or an empty array on error/client not ready. 67 | */ 68 | async function fetchGroupsFromBackend(proactiveUpdate = false) { 69 | if (!backendClientReady) { 70 | console.warn('[BACKGROUND.JS] Cannot fetch groups, backend client not ready.'); 71 | return []; 72 | } 73 | try { 74 | const response = await fetch(`${BACKEND_URL}/groups`); 75 | if (!response.ok) { 76 | let errorMsg = `Backend group list fetch failed: ${response.status}`; 77 | try { 78 | const errorData = await response.json(); 79 | errorMsg = errorData.error || errorData.message || errorMsg; 80 | } catch (e) { /* ignore parsing error */ } 81 | throw new Error(errorMsg); 82 | } 83 | const groups = await response.json(); // Expected: [{ id: string, name: string }] 84 | console.log(`[BACKGROUND.JS] Fetched ${groups.length} groups from backend.`); 85 | backendGroups = groups; 86 | 87 | chrome.storage.local.set({ 'whatsappGroups': backendGroups }); // Cache for potential quick load 88 | 89 | // Panel.js fetches groups on its own. This proactive update is likely redundant. 90 | // if (proactiveUpdate && panelReady) { 91 | // sendMessageToAllTabs({ type: 'GROUPS_UPDATED', groups: backendGroups }); 92 | // } 93 | return backendGroups; 94 | } catch (error) { 95 | console.error('[BACKGROUND.JS] Error fetching groups from backend:', error); 96 | if (proactiveUpdate && panelReady) { 97 | sendMessageToAllTabs({ type: 'WHATSAPP_BACKEND_ERROR', error: `Group fetch error: ${error.message}` }); 98 | } 99 | return []; 100 | } 101 | } 102 | 103 | /** 104 | * Starts polling the backend for WhatsApp client status periodically. 105 | * Calls `fetchWhatsAppStatusFromBackend` immediately and then sets an interval. 106 | */ 107 | function startBackendStatusPolling() { 108 | fetchWhatsAppStatusFromBackend(); // Initial check 109 | setInterval(fetchWhatsAppStatusFromBackend, 15000); // Poll every 15 seconds 110 | } 111 | 112 | startBackendStatusPolling(); 113 | 114 | // --- Message Handling --- 115 | let readyTabs = new Set(); 116 | 117 | /** 118 | * Sends a message to all active and "ready" WhatsApp Web tabs. 119 | * "Ready" tabs are those whose content scripts have reported `CONTENT_SCRIPT_READY`. 120 | * @param {object} message - The message object to send. 121 | */ 122 | function sendMessageToAllTabs(message) { 123 | chrome.tabs.query({ url: '*://web.whatsapp.com/*' }, (tabs) => { 124 | if (tabs.length === 0) { 125 | // console.warn('No WhatsApp Web tabs found to send message:', message.type); 126 | return; 127 | } 128 | tabs.forEach(tab => { 129 | if (readyTabs.has(tab.id)) { 130 | chrome.tabs.sendMessage(tab.id, message) 131 | .catch(err => console.error(`Error sending message type ${message.type} to tab ${tab.id}:`, err.message)); 132 | } else { 133 | // console.log(`Tab ${tab.id} not ready, not sending ${message.type}`); 134 | } 135 | }); 136 | }); 137 | } 138 | 139 | /** 140 | * Listener for messages sent from other parts of the extension (content scripts, panel). 141 | * Handles various message types to coordinate actions, fetch data, or relay information. 142 | * @param {object} message - The message object received. 143 | * @param {chrome.runtime.MessageSender} sender - Information about the sender. 144 | * @param {function} sendResponse - Function to call to send a response back to the sender. 145 | * @returns {boolean} True if `sendResponse` will be called asynchronously, false otherwise. 146 | */ 147 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 148 | console.log('[BACKGROUND.JS] Message received:', message.type, 'from sender:', sender.tab ? `tab ${sender.tab.id}`: "extension"); 149 | 150 | // Use a switch statement for clarity 151 | switch (message.type) { 152 | case 'PANEL_READY': 153 | panelReady = true; 154 | console.log('[BACKGROUND.JS] Panel is ready. Last known backend status:', lastBackendStatus); 155 | // Respond with the last known status. Panel will use this to decide its next steps. 156 | sendResponse({ 157 | success: true, 158 | ready: lastBackendStatus.ready, 159 | message: lastBackendStatus.message, 160 | qrCodeAvailable: lastBackendStatus.qrCodeAvailable 161 | }); 162 | // Proactively send current status to ensure panel updates if it missed polling 163 | if (lastBackendStatus.qrCodeAvailable && !lastBackendStatus.ready) { 164 | sendMessageToAllTabs({ type: 'WHATSAPP_QR_AVAILABLE' }); 165 | } else if (lastBackendStatus.ready) { 166 | sendMessageToAllTabs({ type: 'WHATSAPP_CLIENT_READY' }); 167 | } 168 | break; 169 | 170 | case 'CONTENT_SCRIPT_READY': 171 | if (sender.tab && sender.tab.id !== undefined) { 172 | readyTabs.add(sender.tab.id); 173 | console.log('[BACKGROUND.JS] Tab', sender.tab.id, 'reported CONTENT_SCRIPT_READY. Total ready tabs:', readyTabs.size); 174 | // deliverPendingMessagesToTab(sender.tab.id); // If a pending message system exists 175 | } 176 | sendResponse({ status: 'ACKNOWLEDGED' }); 177 | break; 178 | 179 | default: 180 | console.log('[BACKGROUND.JS] Unhandled message type:', message.type); 181 | sendResponse({ success: false, error: 'Unknown message type' }); 182 | break; 183 | } 184 | // Return true for async responses if any path might be async and doesn't return true itself. 185 | // Most paths above that are async already return true. 186 | return false; 187 | }); 188 | 189 | 190 | // --- Extension Action (Toolbar Icon) Click Handler --- 191 | console.log('[BACKGROUND.JS] Attempting to add chrome.action.onClicked listener...'); 192 | chrome.action.onClicked.addListener((tab) => { 193 | console.log('[BACKGROUND.JS] Extension icon clicked.'); 194 | // We need to ensure this targets only WhatsApp Web tabs, 195 | // or at least that the content script on other pages handles the message gracefully. 196 | // For now, let's assume the content script is only injected on WA Web. 197 | if (tab.url && tab.url.startsWith('https://web.whatsapp.com')) { 198 | // --- Detailed logging for tab readiness --- 199 | console.log(`[BACKGROUND.JS] Action clicked for tab ID: ${tab.id}, URL: ${tab.url}`); 200 | console.log('[BACKGROUND.JS] Current readyTabs:', Array.from(readyTabs)); // Log content of the Set 201 | const isTabReady = readyTabs.has(tab.id); 202 | console.log(`[BACKGROUND.JS] Is this tab (${tab.id}) in readyTabs? ${isTabReady}`); 203 | // --- End detailed logging --- 204 | 205 | if (isTabReady) { 206 | chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_PANEL_VISIBILITY_FROM_ACTION' }) 207 | .then(response => { 208 | if (chrome.runtime.lastError) { 209 | console.error('[BACKGROUND.JS] Error sending TOGGLE_PANEL_VISIBILITY_FROM_ACTION:', chrome.runtime.lastError.message); 210 | } else { 211 | console.log('[BACKGROUND.JS] Sent TOGGLE_PANEL_VISIBILITY_FROM_ACTION, response:', response); 212 | } 213 | }) 214 | .catch(err => console.error('[BACKGROUND.JS] Error sending TOGGLE_PANEL_VISIBILITY_FROM_ACTION:', err.message)); 215 | } else { 216 | console.warn(`[BACKGROUND.JS] WhatsApp tab (ID: ${tab.id}, URL: ${tab.url}) not in readyTabs. Current readyTabs: [${Array.from(readyTabs).join(', ')}]. Cannot send TOGGLE_PANEL_VISIBILITY_FROM_ACTION.`); 217 | // Optionally, try to inject content script or wait for it to become ready. 218 | // For now, we just log. The user might need to refresh the WA page. 219 | } 220 | } else { 221 | console.log('[BACKGROUND.JS] Icon clicked on a non-WhatsApp Web tab:', tab.url); 222 | } 223 | }); 224 | 225 | console.log('Background service worker event listeners attached.'); -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import { 5 | initializeWhatsAppClient, 6 | getQrCode, 7 | isClientReady, 8 | getClientStatusMessage, 9 | getGroups, 10 | getGroupMessages 11 | } from './whatsapp.service'; 12 | import { generateSummary, answerQuestion } from './ai.service'; 13 | import { Message } from 'whatsapp-web.js'; 14 | 15 | // Load environment variables 16 | dotenv.config(); 17 | 18 | const app = express(); 19 | const port = process.env.PORT || 3000; 20 | 21 | // Enable CORS 22 | app.use(cors({ 23 | origin: '*', // Adjust in production 24 | methods: ['GET', 'POST'], 25 | allowedHeaders: ['Content-Type', 'Authorization'] 26 | })); 27 | 28 | // Middleware to parse JSON bodies 29 | app.use(express.json({ limit: '50mb' })); 30 | app.use(express.urlencoded({ limit: '50mb', extended: true })); 31 | 32 | // --- Initialize WhatsApp Client --- 33 | initializeWhatsAppClient() 34 | .then(() => { 35 | console.log("WhatsApp client initialization process started."); 36 | }) 37 | .catch(error => { 38 | console.error("Failed to initialize WhatsApp client after all retries:", error); 39 | // The status message in whatsapp.service should reflect this failure. 40 | }); 41 | 42 | // --- API Endpoints --- 43 | 44 | /** 45 | * @route GET /auth/status 46 | * @description Provides the current status of the WhatsApp client connection. 47 | * @returns {object} JSON object with `ready` (boolean), `message` (string), and `qrCodeAvailable` (boolean). 48 | */ 49 | app.get('/auth/status', (req: Request, res: Response) => { 50 | res.json({ 51 | ready: isClientReady(), 52 | message: getClientStatusMessage(), 53 | qrCodeAvailable: !!getQrCode() && !isClientReady() // QR only relevant if not ready 54 | }); 55 | }); 56 | 57 | /** 58 | * @route GET /auth/qr 59 | * @description Provides the QR code string for WhatsApp authentication, if available and needed. 60 | * @returns {object} JSON object with `qrCode` (string) or an error/status message. 61 | */ 62 | app.get('/auth/qr', (req: Request, res: Response) => { 63 | const qr = getQrCode(); 64 | if (qr && !isClientReady()) { // Only send QR if it's available and client isn't ready 65 | res.json({ qrCode: qr }); 66 | } else if (isClientReady()) { 67 | res.status(200).json({ message: 'Client is already authenticated and ready.' }); 68 | } 69 | else { 70 | res.status(404).json({ error: 'QR code not currently available.' }); 71 | } 72 | }); 73 | 74 | /** 75 | * @route GET /auth/qr-page 76 | * @description Serves an HTML page that displays the current WhatsApp connection status and QR code (if available) for easy scanning. 77 | * The page auto-refreshes every 5 seconds. 78 | */ 79 | app.get('/auth/qr-page', (req: Request, res: Response) => { 80 | res.setHeader('Content-Type', 'text/html'); 81 | const statusMessage = getClientStatusMessage(); 82 | const ready = isClientReady(); 83 | let bodyContent = `

WhatsApp Connection Status

Status: ${statusMessage}

`; 84 | 85 | if (ready) { 86 | bodyContent += '

Client is connected!

'; 87 | } else { 88 | const qr = getQrCode(); 89 | if (qr) { 90 | bodyContent += ` 91 |

Scan this QR code with your phone:

92 |
93 | WhatsApp QR Code 94 |
95 | `; 96 | } else { 97 | bodyContent += '

Waiting for QR code or client to initialize...

'; 98 | } 99 | } 100 | 101 | res.send(` 102 | 103 | 104 | 105 | WhatsApp QR Code 106 | 107 | 112 | 113 | 114 | ${bodyContent} 115 | 116 | 117 | `); 118 | }); 119 | 120 | 121 | /** 122 | * @route GET /groups 123 | * @description Fetches and returns a list of all WhatsApp groups the client is part of. 124 | * @returns {Array} JSON array of group objects, each with `id` and `name`, or an error message. 125 | */ 126 | app.get('/groups', async (req: Request, res: Response) => { 127 | if (!isClientReady()) { 128 | return res.status(503).json({ error: 'WhatsApp client not ready.' }); 129 | } 130 | try { 131 | const groups = await getGroups(); 132 | res.json(groups); 133 | } catch (error: any) { 134 | console.error('Error fetching groups:', error); 135 | res.status(500).json({ error: error.message || 'Failed to fetch groups.' }); 136 | } 137 | }); 138 | 139 | /** 140 | * @route GET /groups/:groupId/messages 141 | * @description Fetches recent messages from a specified group. 142 | * @param {string} req.params.groupId - The ID of the group. 143 | * @param {string} [req.query.count] - Optional number of messages to fetch (defaults to 1000). 144 | * @param {string} [req.query.startDate] - Optional start date (YYYY-MM-DD). 145 | * @param {string} [req.query.endDate] - Optional end date (YYYY-MM-DD). 146 | * @param {string} [req.query.fetchOnlyUnread] - Optional boolean (true/false) to fetch only unread messages. 147 | * @returns {Array} JSON array of formatted message objects, or an error message. 148 | */ 149 | app.get('/groups/:groupId/messages', async (req: Request, res: Response) => { 150 | if (!isClientReady()) { 151 | return res.status(503).json({ error: 'WhatsApp client not ready.' }); 152 | } 153 | const { groupId } = req.params; 154 | const messageCountParam = req.query.count as string; 155 | const startDate = req.query.startDate as string | undefined; 156 | const endDate = req.query.endDate as string | undefined; 157 | const fetchOnlyUnreadParam = req.query.fetchOnlyUnread as string | undefined; 158 | 159 | const fetchOnlyUnread = fetchOnlyUnreadParam === 'true'; 160 | 161 | // Default to 1000 messages if not specified or if date range is used (as a base pool) 162 | const messageCount = messageCountParam ? parseInt(messageCountParam, 10) : 1000; 163 | 164 | if (isNaN(messageCount) || messageCount <= 0) { 165 | return res.status(400).json({ error: 'Invalid message count specified.' }); 166 | } 167 | 168 | try { 169 | const messages = await getGroupMessages(groupId, messageCount, startDate, endDate, fetchOnlyUnread); 170 | // We might want to map messages to a simpler structure for the frontend 171 | const formattedMessages = messages.map(msg => ({ 172 | id: msg.id._serialized, 173 | body: msg.body, 174 | timestamp: msg.timestamp, 175 | from: msg.from, 176 | fromMe: msg.fromMe, 177 | author: msg.author // Important for group messages 178 | })); 179 | res.json(formattedMessages); 180 | } catch (error: any) { 181 | console.error(`Error fetching messages for group ${groupId}:`, error); 182 | res.status(500).json({ error: error.message || `Failed to fetch messages for group ${groupId}.` }); 183 | } 184 | }); 185 | 186 | /** 187 | * @route POST /ai/summarize 188 | * @description Generates an AI-powered summary for messages from a specific chat, optionally within a date range. 189 | * @param {string} req.body.chatId - The ID of the chat. 190 | * @param {string} [req.body.startDate] - Optional start date (YYYY-MM-DD). 191 | * @param {string} [req.body.endDate] - Optional end date (YYYY-MM-DD). 192 | * @param {boolean} [req.body.fetchOnlyUnread] - Optional. If true, attempts to fetch only unread messages for summarization. 193 | * @param {string} [req.body.customPromptText] - Optional. Custom system prompt text. 194 | * @param {string} [req.body.openaiApiKey] - Optional. User-provided OpenAI API key. 195 | * @returns {object} JSON object with the `summary` (string), or an error message. 196 | */ 197 | app.post('/ai/summarize', async (req: Request, res: Response) => { 198 | const { chatId, startDate, endDate, fetchOnlyUnread, customPromptText, openaiApiKey } = req.body as { chatId: string, startDate?: string, endDate?: string, fetchOnlyUnread?: boolean, customPromptText?: string, openaiApiKey?: string }; 199 | 200 | if (!chatId) { 201 | return res.status(400).json({ error: 'Chat ID is required for summarization.' }); 202 | } 203 | if (!isClientReady()) { 204 | return res.status(503).json({ error: 'WhatsApp client not ready.' }); 205 | } 206 | 207 | try { 208 | // Fetch messages using the service, applying date filters and unread flag if provided 209 | // Default to 1000 messages if no date range, or as a base for filtering 210 | const messages = await getGroupMessages(chatId, 1000, startDate, endDate, fetchOnlyUnread); 211 | 212 | if (!messages || messages.length === 0) { 213 | return res.status(404).json({ error: 'No messages found for the given criteria to summarize.' }); 214 | } 215 | 216 | const summary = await generateSummary(messages, startDate, endDate, customPromptText, openaiApiKey); 217 | res.json({ summary }); 218 | } catch (error: any) { 219 | console.error('Error generating summary:', error); 220 | res.status(500).json({ error: error.message || 'Failed to generate summary.' }); 221 | } 222 | }); 223 | 224 | /** 225 | * @route POST /ai/ask 226 | * @description Answers a question based on messages from a specific chat, optionally within a date range. 227 | * @param {string} req.body.chatId - The ID of the chat. 228 | * @param {string} req.body.question - The question to be answered. 229 | * @param {string} [req.body.startDate] - Optional start date (YYYY-MM-DD). 230 | * @param {string} [req.body.endDate] - Optional end date (YYYY-MM-DD). 231 | * @param {boolean} [req.body.fetchOnlyUnread] - Optional. If true, attempts to fetch only unread messages for context. 232 | * @param {string} [req.body.customPromptText] - Optional. Custom system prompt text. 233 | * @param {string} [req.body.openaiApiKey] - Optional. User-provided OpenAI API key. 234 | * @returns {object} JSON object with the `answer` (string), or an error message. 235 | */ 236 | app.post('/ai/ask', async (req: Request, res: Response) => { 237 | const { chatId, question, startDate, endDate, fetchOnlyUnread, customPromptText, openaiApiKey } = req.body as { chatId: string, question: string, startDate?: string, endDate?: string, fetchOnlyUnread?: boolean, customPromptText?: string, openaiApiKey?: string }; 238 | 239 | if (!chatId) { 240 | return res.status(400).json({ error: 'Chat ID is required to answer a question.' }); 241 | } 242 | if (!question || typeof question !== 'string' || question.trim() === "") { 243 | return res.status(400).json({ error: 'A question is required.' }); 244 | } 245 | if (!isClientReady()) { 246 | return res.status(503).json({ error: 'WhatsApp client not ready.' }); 247 | } 248 | 249 | try { 250 | // Fetch messages using the service, applying date filters and unread flag if provided 251 | const messages = await getGroupMessages(chatId, 1000, startDate, endDate, fetchOnlyUnread); 252 | 253 | if (!messages || messages.length === 0) { 254 | return res.status(404).json({ error: 'No messages found for the given criteria to answer the question.' }); 255 | } 256 | 257 | const answer = await answerQuestion(messages, question, customPromptText, openaiApiKey); 258 | res.json({ answer }); 259 | } catch (error: any) { 260 | console.error('Error answering question:', error); 261 | res.status(500).json({ error: error.message || 'Failed to generate answer.' }); 262 | } 263 | }); 264 | 265 | /** 266 | * @route GET /health 267 | * @description Provides a simple health check for the backend service. 268 | * @returns {object} JSON object indicating service status and WhatsApp client status. 269 | */ 270 | app.get('/health', (req: Request, res: Response) => { 271 | res.status(200).json({ 272 | status: 'UP', 273 | message: 'Backend service is running.', 274 | whatsappReady: isClientReady(), 275 | whatsappStatus: getClientStatusMessage() 276 | }); 277 | }); 278 | 279 | 280 | app.listen(port, () => { 281 | console.log(`Backend server listening on port ${port}`); 282 | console.log(`Open http://localhost:${port}/auth/qr-page to see QR code if needed.`); 283 | }); 284 | 285 | // Graceful shutdown 286 | process.on('SIGINT', async () => { 287 | console.log('SIGINT received. Shutting down gracefully...'); 288 | // Add any cleanup logic here if needed for services 289 | // e.g., await client.destroy(); if whatsapp.service exposes client instance or a shutdown method 290 | process.exit(0); 291 | }); -------------------------------------------------------------------------------- /backend/src/whatsapp.service.ts: -------------------------------------------------------------------------------- 1 | import { Client, LocalAuth, Message, Chat, GroupChat } from 'whatsapp-web.js'; 2 | import qrcode from 'qrcode-terminal'; 3 | 4 | let client: Client; 5 | let qrCodeData: string | null = null; 6 | let clientReady = false; 7 | let clientStatusMessage = "Initializing..."; 8 | 9 | /** 10 | * Initializes the WhatsApp client using `whatsapp-web.js`. 11 | * Sets up event listeners for QR codes, authentication, readiness, and disconnection. 12 | * Implements a retry mechanism for initialization failures. 13 | * @param {number} [retries=3] - The number of retries for initialization. 14 | * @returns {Promise} A promise that resolves with the initialized client instance. 15 | * @throws Will throw an error if initialization fails after all retries. 16 | */ 17 | const initializeWhatsAppClient = async (retries = 3): Promise => { 18 | console.log('Initializing WhatsApp client...'); 19 | const newClient = new Client({ 20 | authStrategy: new LocalAuth({ 21 | dataPath: ".wwebjs_auth" 22 | }), 23 | puppeteer: { 24 | headless: true, 25 | args: [ 26 | '--no-sandbox', 27 | '--disable-setuid-sandbox', 28 | '--disable-dev-shm-usage' 29 | ], 30 | timeout: 120000 31 | }, 32 | webVersionCache: { 33 | type: 'none' 34 | }, 35 | restartOnAuthFail: true 36 | }); 37 | 38 | newClient.on('qr', (qr: string) => { 39 | console.log('QR Code Received, scan it with your phone (also available at GET /auth/qr):'); 40 | qrcode.generate(qr, { small: true }); 41 | qrCodeData = qr; 42 | clientStatusMessage = "QR code received. Please scan."; 43 | clientReady = false; // Not ready until authenticated 44 | }); 45 | 46 | newClient.on('loading_screen', (percent: string, message: string) => { 47 | console.log('LOADING SCREEN', percent, message); 48 | clientStatusMessage = `Loading: ${percent}% - ${message}`; 49 | }); 50 | 51 | newClient.on('authenticated', () => { 52 | console.log('Client is authenticated!'); 53 | clientStatusMessage = "Authenticated successfully."; 54 | qrCodeData = null; // Clear QR code once authenticated 55 | }); 56 | 57 | newClient.on('auth_failure', (msg: string) => { 58 | console.error('Authentication failure:', msg); 59 | clientStatusMessage = `Authentication failure: ${msg}. Consider deleting .wwebjs_auth folder and restarting.`; 60 | clientReady = false; 61 | // Potentially trigger re-initialization or clear session data 62 | // For now, we rely on restartOnAuthFail or manual restart 63 | }); 64 | 65 | newClient.on('ready', () => { 66 | console.log('WhatsApp client is ready!'); 67 | clientReady = true; 68 | clientStatusMessage = "Client is ready and connected to WhatsApp."; 69 | }); 70 | 71 | newClient.on('disconnected', (reason: string) => { 72 | console.log('Client was logged out or disconnected:', reason); 73 | clientReady = false; 74 | qrCodeData = null; // QR might be needed again 75 | clientStatusMessage = `Client disconnected: ${reason}. Attempting to reconnect...`; 76 | // whatsapp-web.js might handle reconnection automatically. 77 | // If not, a more robust retry/re-initiation logic might be needed here. 78 | // For now, we'll let it try to reconnect or require a server restart if it fails persistently. 79 | }); 80 | 81 | try { 82 | console.log('Attempting to initialize WhatsApp client instance...'); 83 | await newClient.initialize(); 84 | client = newClient; // Assign to the outer scope client variable 85 | return newClient; 86 | } catch (err: any) { 87 | console.error("Client initialization error:", err); 88 | clientStatusMessage = `Client initialization error: ${err?.message || 'Unknown error'}`; 89 | if (retries > 0) { 90 | console.log(`Initialization failed. Retrying in 10 seconds... (${retries} retries left)`); 91 | await new Promise(resolve => setTimeout(resolve, 10000)); 92 | return initializeWhatsAppClient(retries - 1); 93 | } else { 94 | console.error("Maximum retries reached. Could not initialize WhatsApp client."); 95 | // Keep clientStatusMessage as is, indicating the final error. 96 | throw err; // Propagate the error to be handled by the caller 97 | } 98 | } 99 | }; 100 | 101 | /** 102 | * Retrieves the initialized WhatsApp client instance. 103 | * @returns {Client} The WhatsApp client instance. 104 | * @throws Will throw an error if the client is not initialized. 105 | */ 106 | const getClient = (): Client => { 107 | if (!client) { 108 | throw new Error("WhatsApp client is not initialized."); 109 | } 110 | return client; 111 | }; 112 | 113 | /** 114 | * Gets the current QR code data for authentication. 115 | * @returns {string | null} The QR code string, or null if not available. 116 | */ 117 | const getQrCode = (): string | null => qrCodeData; 118 | 119 | /** 120 | * Checks if the WhatsApp client is ready and connected. 121 | * @returns {boolean} True if the client is ready, false otherwise. 122 | */ 123 | const isClientReady = (): boolean => clientReady; 124 | 125 | /** 126 | * Gets the current status message of the WhatsApp client. 127 | * @returns {string} The client status message. 128 | */ 129 | const getClientStatusMessage = (): string => clientStatusMessage; 130 | 131 | /** 132 | * Fetches a list of all groups the client is a part of. 133 | * @returns {Promise>} A promise that resolves to an array of group objects, each with an id and name. 134 | * @throws Will throw an error if the client is not ready. 135 | */ 136 | const getGroups = async (): Promise<{ id: string; name: string }[]> => { 137 | if (!isClientReady()) { 138 | throw new Error("WhatsApp client not ready."); 139 | } 140 | console.log('[WHATSAPP.SERVICE] getGroups: Entered function.'); // Added log 141 | const currentClient = getClient(); 142 | try { 143 | console.log('[WHATSAPP.SERVICE] getGroups: Calling currentClient.getChats()...'); // Added log 144 | const chats = await currentClient.getChats(); 145 | console.log(`[WHATSAPP.SERVICE] getGroups: currentClient.getChats() resolved. Found ${chats.length} chats.`); // Added log 146 | 147 | const groupChats = chats.filter(chat => chat.isGroup); 148 | console.log(`[WHATSAPP.SERVICE] getGroups: Filtered to ${groupChats.length} group chats.`); // Added log 149 | 150 | const result = groupChats.map(chat => ({ 151 | id: chat.id._serialized, 152 | name: chat.name || chat.id.user, 153 | })); 154 | console.log('[WHATSAPP.SERVICE] getGroups: Mapped group chats. Returning result.'); // Added log 155 | return result; 156 | } catch (error) { 157 | console.error('[WHATSAPP.SERVICE] getGroups: Error during getChats or processing:', error); // Added log 158 | throw error; // Re-throw the error to be caught by the route handler 159 | } 160 | }; 161 | 162 | /** 163 | * Fetches a specified number of recent messages from a given group. 164 | * @param {string} groupId - The ID of the group to fetch messages from. 165 | * @param {number} [messageCount=1000] - The number of messages to fetch if no date range is specified, or the initial batch size if dates are specified. 166 | * @param {string} [startDate] - Optional start date string (e.g., "YYYY-MM-DD"). 167 | * @param {string} [endDate] - Optional end date string (e.g., "YYYY-MM-DD"). 168 | * @param {boolean} [fetchOnlyUnread] - Optional. If true, attempts to fetch only unread messages. 169 | * @returns {Promise} A promise that resolves to an array of `whatsapp-web.js` Message objects. 170 | * @throws Will throw an error if the client is not ready, or if the group is not found or messages cannot be fetched. 171 | */ 172 | const getGroupMessages = async ( 173 | groupId: string, 174 | messageCount: number = 1000, 175 | startDate?: string, 176 | endDate?: string, 177 | fetchOnlyUnread?: boolean 178 | ): Promise => { 179 | if (!isClientReady()) { 180 | throw new Error("WhatsApp client not ready."); 181 | } 182 | const currentClient = getClient(); 183 | try { 184 | console.log(`[WHATSAPP.SERVICE] getGroupMessages called with: groupId=${groupId}, messageCount=${messageCount}, startDate=${startDate}, endDate=${endDate}, fetchOnlyUnread=${fetchOnlyUnread}`); 185 | const chat = await currentClient.getChatById(groupId); 186 | if (!chat || !chat.isGroup) { 187 | throw new Error(`Group with ID ${groupId} not found or is not a group.`); 188 | } 189 | console.log(`[WHATSAPP.SERVICE] Fetched chat: ${chat.id._serialized}, Unread count: ${chat.unreadCount}`); 190 | 191 | // Fetch messages, whatsapp-web.js fetches recent messages by default with limit 192 | // If dates are provided, we might fetch a larger batch to ensure coverage, then filter. 193 | // If date filtering is active, messageCount effectively becomes the max pool size for filtering. 194 | 195 | let effectiveMessageCount = messageCount; 196 | console.log(`[WHATSAPP.SERVICE] Initial effectiveMessageCount: ${effectiveMessageCount}`); 197 | 198 | if (fetchOnlyUnread && chat.unreadCount && chat.unreadCount > 0) { 199 | console.log(`[WHATSAPP.SERVICE] fetchOnlyUnread is TRUE. Using chat.unreadCount (${chat.unreadCount}) as effectiveMessageCount.`); 200 | effectiveMessageCount = chat.unreadCount; 201 | } else if (fetchOnlyUnread) { 202 | console.log(`[WHATSAPP.SERVICE] fetchOnlyUnread is TRUE, but chat.unreadCount is ${chat.unreadCount}. Defaulting to initial messageCount (${messageCount}) for effectiveMessageCount.`); 203 | // effectiveMessageCount remains messageCount as per current logic 204 | } else { 205 | console.log(`[WHATSAPP.SERVICE] fetchOnlyUnread is FALSE. Using initial messageCount (${messageCount}) for effectiveMessageCount.`); 206 | } 207 | console.log(`[WHATSAPP.SERVICE] After unread check, effectiveMessageCount: ${effectiveMessageCount}`); 208 | 209 | // Ensure a decent pool if date filtering is active, potentially overriding unread count if date range is wide 210 | const messagesToFetch = (startDate && endDate) ? Math.max(effectiveMessageCount, 1000) : effectiveMessageCount; 211 | console.log(`[WHATSAPP.SERVICE] Final messagesToFetch (considering date filters influencing pool size): ${messagesToFetch}`); 212 | 213 | let messages = await chat.fetchMessages({ limit: messagesToFetch }); 214 | console.log(`[WHATSAPP.SERVICE] Fetched ${messages.length} messages initially (before date filtering).`); 215 | 216 | if (startDate && endDate) { 217 | console.log(`[WHATSAPP.SERVICE] Filtering messages for group ${groupId} between ${startDate} (DD/MM/YYYY) and ${endDate} (DD/MM/YYYY)`); 218 | 219 | const parseDdMmYyyy = (dateString: string): Date | null => { 220 | const parts = dateString.split('/'); 221 | if (parts.length === 3) { 222 | const day = parseInt(parts[0], 10); 223 | const month = parseInt(parts[1], 10) - 1; // Month is 0-indexed in JS Date 224 | const year = parseInt(parts[2], 10); 225 | if (!isNaN(day) && !isNaN(month) && !isNaN(year)) { 226 | return new Date(year, month, day); 227 | } 228 | } 229 | return null; 230 | }; 231 | 232 | const startDateObj = parseDdMmYyyy(startDate); 233 | const endDateObj = parseDdMmYyyy(endDate); 234 | 235 | if (!startDateObj || !endDateObj) { 236 | console.warn(`[WHATSAPP.SERVICE] Invalid DD/MM/YYYY date format provided for group ${groupId}. Start: ${startDate}, End: ${endDate}. Returning all fetched messages.`); 237 | } else { 238 | const startTimestamp = startDateObj.setHours(0, 0, 0, 0) / 1000; 239 | const endTimestamp = endDateObj.setHours(23, 59, 59, 999) / 1000; 240 | 241 | messages = messages.filter(msg => { 242 | // Assuming msg.timestamp is a Unix timestamp in seconds 243 | return msg.timestamp >= startTimestamp && msg.timestamp <= endTimestamp; 244 | }); 245 | console.log(`[WHATSAPP.SERVICE] Found ${messages.length} messages after DD/MM/YYYY date filtering for group ${groupId}.`); 246 | } 247 | } 248 | 249 | return messages.map(msg => msg); // Ensure it's an array of Message instances 250 | } catch (error) { 251 | console.error(`Error fetching messages for group ${groupId}:`, error); 252 | throw new Error(`Failed to fetch messages for group ${groupId}.`); 253 | } 254 | }; 255 | 256 | // Export functions and variables to be used by other parts of the application 257 | export { 258 | initializeWhatsAppClient, 259 | getClient, 260 | getQrCode, 261 | isClientReady, 262 | getClientStatusMessage, 263 | getGroups, 264 | getGroupMessages, 265 | // Export client instance directly if needed elsewhere, though getClient() is preferred 266 | // client as whatsAppClientInstance 267 | }; -------------------------------------------------------------------------------- /extension/panel.css: -------------------------------------------------------------------------------- 1 | /* Tailwind-inspired Modern UI - General Panel Styling */ 2 | :root { 3 | --wa-green: #00a884; 4 | --wa-green-dark: #008069; 5 | --wa-light-green-bg: #e5ddd5; /* WhatsApp chat background */ 6 | --wa-message-user: #dcf8c6; 7 | --wa-message-assistant: #ffffff; 8 | --text-primary: #111b21; 9 | --text-secondary: #54656f; 10 | --text-light: #667781; 11 | --border-color: #d1d7db; 12 | --bg-panel: #f7f9fa; 13 | --bg-input-area: #f0f2f5; 14 | --bg-controls: #ffffff; 15 | --font-sans: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | html[dir="rtl"] body { 19 | font-family: var(--font-sans); /* Consistent font */ 20 | } 21 | 22 | body { 23 | font-family: var(--font-sans); 24 | margin: 0; 25 | background-color: var(--bg-input-area); /* Match WA input area for body */ 26 | color: var(--text-primary); 27 | display: flex; 28 | height: 100vh; 29 | overflow: hidden; 30 | font-size: 14px; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | } 34 | 35 | #ai-copilot-panel { 36 | width: 360px; /* Default width, can be adjusted by user */ 37 | height: 100%; 38 | background-color: var(--bg-panel); 39 | border-right: 1px solid var(--border-color); /* For RTL */ 40 | box-shadow: 2px 0 5px rgba(0,0,0,0.05); /* Softer shadow */ 41 | display: flex; 42 | flex-direction: column; 43 | transition: width 0.2s ease; /* Only width transition needed now */ 44 | } 45 | 46 | #ai-copilot-panel.collapsed { 47 | width: 0 !important; /* Ensure it collapses fully */ 48 | overflow: hidden; 49 | padding: 0; 50 | border-right: none; 51 | box-shadow: none; 52 | } 53 | 54 | .panel-header { 55 | background-color: var(--wa-green-dark); 56 | color: white; 57 | padding: 12px 16px; /* Tailwind-like spacing */ 58 | display: flex; 59 | justify-content: space-between; 60 | align-items: center; 61 | flex-shrink: 0; 62 | /* box-shadow: 0 1px 2px rgba(0,0,0,0.05); */ /* Subtle shadow or none */ 63 | } 64 | 65 | .panel-title { 66 | font-size: 18px; /* Slightly smaller */ 67 | font-weight: 500; /* Medium weight */ 68 | display: flex; 69 | align-items: center; 70 | } 71 | .panel-title .fab.fa-whatsapp { 72 | margin-left: 8px; /* For RTL */ 73 | margin-right: 0; 74 | font-size: 22px; 75 | } 76 | 77 | .fold-button { 78 | background: none; 79 | border: none; 80 | color: white; 81 | font-size: 20px; /* Larger icon for better tap target */ 82 | cursor: pointer; 83 | padding: 6px; 84 | border-radius: 4px; 85 | transition: background-color 0.15s ease; 86 | } 87 | .fold-button:hover { 88 | background-color: rgba(255,255,255,0.1); 89 | } 90 | 91 | /* Top Controls Area */ 92 | .panel-top-controls { 93 | padding: 12px 16px; 94 | background-color: var(--bg-controls); 95 | border-bottom: 1px solid var(--border-color); 96 | flex-shrink: 0; 97 | display: flex; 98 | flex-direction: column; 99 | gap: 12px; /* Space between control groups */ 100 | } 101 | 102 | /* Chat Area */ 103 | .chat-area-wrapper { 104 | flex-grow: 1; 105 | overflow-y: auto; 106 | padding: 16px; /* More padding */ 107 | background-color: var(--wa-light-green-bg); 108 | display: flex; 109 | flex-direction: column-reverse; /* New messages at bottom, scroll up for history */ 110 | } 111 | 112 | .message-list { 113 | display: flex; 114 | flex-direction: column; /* Messages stack normally now due to parent reverse */ 115 | gap: 10px; 116 | width: 100%; 117 | } 118 | 119 | /* Chat Input Area */ 120 | .chat-input-area { 121 | display: flex; 122 | align-items: flex-end; /* Align items to bottom for multi-line textarea */ 123 | padding: 12px 16px; 124 | background-color: var(--bg-input-area); 125 | border-top: 1px solid var(--border-color); 126 | flex-shrink: 0; 127 | gap: 8px; /* Space between textarea and button */ 128 | } 129 | 130 | .chat-input-area textarea#ai-query-input { 131 | flex-grow: 1; 132 | padding: 10px 16px; /* More padding */ 133 | border: 1px solid #ccd0d5; /* Slightly different border */ 134 | border-radius: 18px; /* Consistent rounded corners */ 135 | font-size: 15px; 136 | resize: none; 137 | min-height: 24px; /* Based on line-height + padding */ 138 | max-height: 120px; 139 | line-height: 1.5; 140 | box-sizing: border-box; 141 | background-color: var(--bg-controls); 142 | color: var(--text-primary); 143 | } 144 | .chat-input-area textarea#ai-query-input::placeholder { 145 | color: var(--text-light); 146 | } 147 | .chat-input-area textarea#ai-query-input:focus { 148 | outline: none; 149 | border-color: var(--wa-green); 150 | box-shadow: 0 0 0 2px rgba(0, 168, 132, 0.2); 151 | } 152 | 153 | 154 | .chat-input-area button#ask-question-button { 155 | background-color: var(--wa-green); 156 | color: white; 157 | border: none; 158 | border-radius: 50%; 159 | width: 44px; /* Slightly larger */ 160 | height: 44px; 161 | font-size: 20px; 162 | display: flex; 163 | align-items: center; 164 | justify-content: center; 165 | cursor: pointer; 166 | transition: background-color 0.2s ease; 167 | flex-shrink: 0; /* Prevent button from shrinking */ 168 | padding: 0; 169 | margin: 0; 170 | } 171 | .chat-input-area button#ask-question-button i { 172 | margin: 0; 173 | } 174 | .chat-input-area button#ask-question-button:hover:not(:disabled) { 175 | background-color: var(--wa-green-dark); 176 | } 177 | .chat-input-area button#ask-question-button:disabled { 178 | background-color: #bcc0c4; /* Lighter disabled color */ 179 | cursor: not-allowed; 180 | } 181 | 182 | /* Status Section in Top Controls */ 183 | .status-section { 184 | padding: 0; /* No extra padding, handled by parent */ 185 | margin-bottom: 0; /* No margin, handled by gap */ 186 | background-color: transparent; 187 | border-radius: 0; 188 | text-align: center; 189 | border: none; 190 | box-shadow: none; 191 | } 192 | #whatsapp-connection-status { 193 | font-weight: 500; 194 | margin-bottom: 8px; 195 | font-size: 0.9em; /* Smaller status text */ 196 | color: var(--text-secondary); 197 | } 198 | #whatsapp-qr-code-area p { 199 | font-size: 0.85em; 200 | color: var(--text-secondary); 201 | margin-bottom: 6px; 202 | } 203 | #whatsapp-qr-code-img { 204 | background-color: white; 205 | border: 1px solid var(--border-color); 206 | padding: 8px; 207 | border-radius: 6px; 208 | display: inline-block; 209 | box-shadow: 0 1px 2px rgba(0,0,0,0.05); 210 | } 211 | 212 | /* Group Selector in Top Controls */ 213 | .control-section label { /* For "Select Group" */ 214 | display: block; 215 | font-weight: 500; 216 | margin-bottom: 6px; 217 | color: var(--text-primary); 218 | font-size: 0.9em; 219 | } 220 | select#chat-selector { 221 | width: 100%; 222 | padding: 8px 12px; 223 | border: 1px solid #ccd0d5; 224 | border-radius: 6px; 225 | font-size: 14px; 226 | box-sizing: border-box; 227 | background-color: var(--bg-controls); 228 | color: var(--text-primary); 229 | } 230 | select#chat-selector:focus { 231 | outline: none; 232 | border-color: var(--wa-green); 233 | box-shadow: 0 0 0 2px rgba(0, 168, 132, 0.2); 234 | } 235 | 236 | /* General Button (e.g., Summarize Group) */ 237 | .button { 238 | background-color: var(--wa-green); 239 | color: white; 240 | border: none; 241 | padding: 8px 16px; /* Tailwind-like padding */ 242 | border-radius: 6px; /* Standard border radius */ 243 | font-size: 14px; 244 | font-weight: 500; /* Medium weight */ 245 | cursor: pointer; 246 | transition: background-color 0.15s ease; 247 | display: flex; 248 | align-items: center; 249 | justify-content: center; 250 | /* box-shadow: 0 1px 2px rgba(0,0,0,0.05); */ /* Optional subtle shadow */ 251 | } 252 | .button:hover:not(:disabled) { 253 | background-color: var(--wa-green-dark); 254 | } 255 | .button:disabled { 256 | background-color: #e0e0e0; /* Lighter disabled */ 257 | color: #a0a0a0; 258 | cursor: not-allowed; 259 | } 260 | .button i { 261 | font-size: 1.1em; /* Relative to button font size */ 262 | } 263 | html[dir="rtl"] .button i { 264 | margin-right: 8px; 265 | margin-left: 0; 266 | } 267 | html[dir="ltr"] .button i { 268 | margin-left: 8px; 269 | margin-right: 0; 270 | } 271 | .button.full-width { 272 | width: 100%; 273 | } 274 | 275 | /* Message Styling */ 276 | .message { 277 | padding: 10px 14px; /* Consistent padding */ 278 | border-radius: 12px; /* Softer, modern radius */ 279 | margin-bottom: 0; /* Gap handled by message-list */ 280 | max-width: 85%; /* More space for messages */ 281 | word-wrap: break-word; 282 | font-size: 14.5px; /* Slightly larger message font */ 283 | line-height: 1.45; 284 | box-shadow: 0 1px 1.5px rgba(0,0,0,0.07); /* Subtle shadow */ 285 | position: relative; /* For potential future elements like timestamps */ 286 | } 287 | .message strong { /* Sender Name (You, AI Assistant) */ 288 | display: block; 289 | font-weight: 600; /* Semibold */ 290 | margin-bottom: 4px; 291 | font-size: 0.8em; /* Smaller, distinct sender name */ 292 | color: var(--text-primary); /* Darker for better contrast on bubbles */ 293 | opacity: 0.8; 294 | } 295 | 296 | .user-message { 297 | background-color: var(--wa-message-user); 298 | color: var(--text-primary); /* Ensure text is readable */ 299 | border-bottom-left-radius: 4px; /* WhatsApp style "tail" */ 300 | } 301 | html[dir="rtl"] .user-message { 302 | align-self: flex-end; 303 | margin-left: auto; 304 | margin-right: 0; 305 | border-bottom-right-radius: 4px; 306 | border-bottom-left-radius: 12px; 307 | } 308 | html[dir="ltr"] .user-message { 309 | align-self: flex-end; 310 | margin-right: auto; 311 | margin-left: 0; 312 | } 313 | 314 | 315 | .assistant-message { 316 | background-color: var(--wa-message-assistant); 317 | color: var(--text-primary); 318 | border: 1px solid #f0f0f0; /* Softer border for assistant messages */ 319 | border-bottom-right-radius: 4px; /* WhatsApp style "tail" */ 320 | } 321 | html[dir="rtl"] .assistant-message { 322 | align-self: flex-start; 323 | margin-right: auto; 324 | margin-left: 0; 325 | border-bottom-left-radius: 4px; 326 | border-bottom-right-radius: 12px; 327 | } 328 | html[dir="ltr"] .assistant-message { 329 | align-self: flex-start; 330 | margin-left: auto; 331 | margin-right: 0; 332 | } 333 | 334 | 335 | .assistant-message.error-message { 336 | background-color: #fff0f0; 337 | border-color: #ffdddd; 338 | color: #d8000c; 339 | } 340 | .assistant-message.error-message strong { 341 | color: #a00000; 342 | } 343 | 344 | /* Styles for AI-generated HTML content within assistant messages */ 345 | .assistant-message .message-content h2 { 346 | font-size: 1.15em; /* Larger than base message text, but not huge */ 347 | font-weight: 600; /* Semibold */ 348 | color: var(--text-primary); /* Or a specific title color */ 349 | margin-top: 10px; 350 | margin-bottom: 8px; 351 | padding-bottom: 4px; 352 | border-bottom: 1px solid var(--border-color); /* Subtle separator */ 353 | } 354 | .assistant-message .message-content h2:first-child { 355 | margin-top: 0; /* No top margin for the very first title */ 356 | } 357 | 358 | .assistant-message .message-content p { 359 | margin-top: 0; 360 | margin-bottom: 0.75em; /* Space between paragraphs */ 361 | line-height: 1.5; /* Improved readability for paragraphs */ 362 | } 363 | .assistant-message .message-content p:last-child { 364 | margin-bottom: 0; 365 | } 366 | 367 | .assistant-message .message-content strong { 368 | font-weight: 600; /* Ensure strong is bolder than normal text */ 369 | color: var(--text-primary); /* Or inherit */ 370 | } 371 | 372 | .assistant-message .message-content em { 373 | font-style: italic; 374 | } 375 | 376 | .assistant-message .message-content ul { 377 | list-style-type: disc; /* Standard bullets */ 378 | margin-top: 0.5em; 379 | margin-bottom: 0.75em; 380 | padding-left: 20px; /* Indent list items */ 381 | } 382 | html[dir="rtl"] .assistant-message .message-content ul { 383 | padding-left: 0; 384 | padding-right: 20px; /* Indent list items for RTL */ 385 | } 386 | 387 | 388 | .assistant-message .message-content li { 389 | margin-bottom: 0.4em; /* Space between list items */ 390 | } 391 | 392 | .assistant-message .message-content a { 393 | color: var(--wa-green-dark); /* Use a theme color for links */ 394 | text-decoration: underline; 395 | font-weight: 500; 396 | } 397 | .assistant-message .message-content a:hover { 398 | color: var(--wa-green); /* Darken on hover */ 399 | text-decoration: none; 400 | } 401 | /* End of styles for AI-generated HTML content */ 402 | 403 | .system-message { 404 | text-align: center; 405 | font-size: 0.75em; /* Smaller system messages */ 406 | color: var(--text-light); 407 | margin: 12px auto; /* Centered with more margin */ 408 | padding: 4px 8px; 409 | background-color: rgba(0,0,0,0.03); 410 | border-radius: 4px; 411 | max-width: fit-content; 412 | } 413 | 414 | /* AI Status Message (below input) */ 415 | #ai-status-message.status-message { /* More specific selector */ 416 | font-size: 0.8em; 417 | margin-top: 6px; /* Closer to input */ 418 | padding: 6px 10px; 419 | border-radius: 4px; 420 | text-align: center; 421 | width: 100%; 422 | box-sizing: border-box; 423 | display: block; /* Ensure it takes width if content is short */ 424 | } 425 | /* ... (status-ok, status-error styles remain similar but can be tweaked) */ 426 | 427 | 428 | /* Footer */ 429 | .panel-footer { 430 | padding: 8px 16px; /* Reduced padding */ 431 | border-top: 1px solid var(--border-color); 432 | background-color: var(--bg-input-area); 433 | display: flex; 434 | justify-content: space-between; 435 | align-items: center; 436 | flex-shrink: 0; 437 | } 438 | #debug-connection-info { 439 | font-size: 0.75em; /* Smaller debug info */ 440 | color: var(--text-light); 441 | } 442 | .button-icon { /* For refresh button in footer */ 443 | background: none; 444 | border: none; 445 | color: var(--text-secondary); 446 | font-size: 18px; /* Larger icon */ 447 | padding: 4px; 448 | cursor: pointer; 449 | border-radius: 4px; 450 | transition: color 0.15s ease, background-color 0.15s ease; 451 | } 452 | .button-icon:hover { 453 | color: var(--text-primary); 454 | background-color: rgba(0,0,0,0.05); 455 | } 456 | 457 | /* Spinner */ 458 | .spinner-small { 459 | width: 16px; /* Standard size */ 460 | height: 16px; 461 | border: 2px solid rgba(0,0,0,0.1); 462 | border-left-color: var(--wa-green); /* Use theme color */ 463 | border-radius: 50%; 464 | display: inline-block; 465 | animation: spin 0.7s linear infinite; 466 | } 467 | html[dir="rtl"] .spinner-small { 468 | margin-right: 8px; /* Consistent spacing */ 469 | margin-left: 0; 470 | } 471 | html[dir="ltr"] .spinner-small { 472 | margin-left: 8px; 473 | margin-right: 0; 474 | } 475 | 476 | @keyframes spin { 477 | to { transform: rotate(360deg); } 478 | } 479 | 480 | .hidden { 481 | display: none !important; 482 | } 483 | 484 | /* FontAwesome base */ 485 | .fa, .fas, .far, .fal, .fab { 486 | display: inline-block; 487 | font-style: normal; 488 | font-variant: normal; 489 | text-rendering: auto; 490 | line-height: 1; /* Ensure consistent line height */ 491 | } -------------------------------------------------------------------------------- /extension/content.js: -------------------------------------------------------------------------------- 1 | console.log("WhatsApp AI Co-Pilot content script loaded."); 2 | 3 | // We will add logic here to: 4 | // 1. Inject the sidebar UI (Step 5) 5 | // 2. Observe WhatsApp Web for new messages or chat changes (Step 6) 6 | // 3. Communicate with the background script 7 | 8 | const SIDEBAR_ID = 'whatsapp-ai-copilot-sidebar'; 9 | let sidebarInjected = false; 10 | 11 | /** 12 | * Injects the AI Co-Pilot sidebar iframe into the WhatsApp Web page. 13 | * Sets up styles and basic communication listeners for the iframe. 14 | * Ensures it only injects once. 15 | */ 16 | function injectSidebar() { 17 | if (document.getElementById(SIDEBAR_ID) || sidebarInjected) { 18 | console.log("Sidebar already injected."); 19 | return; 20 | } 21 | 22 | console.log("Attempting to inject sidebar..."); 23 | 24 | // Create a container for the entire sidebar 25 | const sidebarContainer = document.createElement('div'); 26 | sidebarContainer.id = SIDEBAR_ID; 27 | 28 | // --- State Management --- 29 | const FIXED_PANEL_WIDTH = 375; // Fixed panel width in pixels 30 | let isSidebarCollapsed = localStorage.getItem('whatsappAiCopilotSidebarCollapsed') === 'true'; 31 | // currentSidebarSide is fixed to 'right' and width is fixed, so no need for these in localStorage 32 | 33 | function saveSidebarState() { 34 | localStorage.setItem('whatsappAiCopilotSidebarCollapsed', isSidebarCollapsed); 35 | // No longer saving width as it's fixed 36 | } 37 | // --- End State Management --- 38 | 39 | // Initial setup - panel is always on the right 40 | sidebarContainer.style.position = 'fixed'; 41 | sidebarContainer.style.top = '0'; 42 | sidebarContainer.style.right = '0'; // Fixed to right 43 | sidebarContainer.style.left = 'auto'; 44 | sidebarContainer.style.height = '100vh'; 45 | sidebarContainer.style.zIndex = '1000'; 46 | sidebarContainer.style.background = '#fff'; 47 | sidebarContainer.style.display = 'flex'; 48 | sidebarContainer.style.flexDirection = 'column'; 49 | sidebarContainer.style.overflow = 'visible'; 50 | sidebarContainer.style.boxShadow = '-2px 0 8px rgba(0,0,0,0.08)'; 51 | 52 | 53 | // Apply width based on loaded state 54 | function applySidebarStyling() { 55 | if (isSidebarCollapsed) { 56 | sidebarContainer.style.width = '0px'; 57 | } else { 58 | sidebarContainer.style.width = `${FIXED_PANEL_WIDTH}px`; 59 | } 60 | } 61 | applySidebarStyling(); 62 | 63 | // Resizer element and its logic are removed for fixed width 64 | 65 | // Create an iframe to load panel.html in extension context 66 | const iframe = document.createElement('iframe'); 67 | iframe.src = chrome.runtime.getURL('panel.html'); 68 | iframe.style.cssText = ` 69 | height: 100%; 70 | width: 100%; 71 | border: none; 72 | background-color: white; 73 | flex: 1; 74 | `; 75 | iframe.id = "whatsapp-ai-copilot-iframe"; 76 | sidebarContainer.appendChild(iframe); 77 | 78 | // Function to adjust WhatsApp Web layout 79 | function adjustWhatsAppLayout() { 80 | const appElement = document.getElementById('app'); 81 | 82 | if (appElement) { 83 | appElement.style.transition = 'width 0.2s ease-out, margin-right 0.2s ease-out'; // Smooth transition 84 | if (isSidebarCollapsed) { 85 | appElement.style.removeProperty('width'); 86 | appElement.style.removeProperty('margin-right'); 87 | } else { 88 | appElement.style.setProperty('width', `calc(100% - ${FIXED_PANEL_WIDTH}px)`, 'important'); 89 | appElement.style.setProperty('margin-right', `${FIXED_PANEL_WIDTH}px`, 'important'); 90 | } 91 | } 92 | 93 | // Attempt to prevent horizontal scroll on the main document 94 | document.documentElement.style.overflowX = 'hidden'; 95 | document.body.style.overflowX = 'hidden'; 96 | } 97 | 98 | // Initial adjustment 99 | adjustWhatsAppLayout(); 100 | 101 | 102 | // Inject into the body of the page 103 | document.body.appendChild(sidebarContainer); 104 | sidebarInjected = true; 105 | console.log("Sidebar container and iframe injected. Initial state:", { panelWidth: FIXED_PANEL_WIDTH, isSidebarCollapsed }); 106 | 107 | // Send initial state to panel.js 108 | if (iframe.contentWindow) { 109 | iframe.onload = () => { 110 | iframe.contentWindow.postMessage({ action: 'updatePanelControls', side: 'right', isCollapsed: isSidebarCollapsed }, '*'); // Side is fixed 111 | }; 112 | } else { 113 | setTimeout(() => { 114 | if (iframe.contentWindow) { 115 | iframe.contentWindow.postMessage({ action: 'updatePanelControls', side: 'right', isCollapsed: isSidebarCollapsed }, '*'); // Side is fixed 116 | } 117 | }, 200); 118 | } 119 | 120 | 121 | // Resizer logic removed 122 | 123 | // Remove the old static style injection if it exists 124 | const oldStyle = document.head.querySelector('style[data-whatsapp-ai-copilot-style]'); 125 | if (oldStyle) { 126 | oldStyle.remove(); 127 | } 128 | // Add class to document to help with styling WhatsApp Web 129 | // document.documentElement.classList.add('whatsapp-ai-sidebar-active'); // This might not be needed if we directly set margins 130 | // Add a listener for messages from the iframe 131 | window.addEventListener('message', (event) => { 132 | // Temporarily log ALL messages to see what's coming in 133 | // const iframe = document.getElementById('whatsapp-ai-copilot-iframe'); // REMOVE THIS LINE - use iframe from outer scope 134 | console.log('[Content Script] Raw message received by window:', { data: event.data, origin: event.origin, source: event.source === iframe?.contentWindow ? 'matches_iframe' : 'does_not_match_iframe_or_iframe_undefined' }); 135 | 136 | if (event.source === iframe?.contentWindow) { 137 | try { 138 | // This log is now redundant due to the one above, but keep for specific iframe source confirmation if needed later. 139 | // console.log('[Content Script] Message received from iframe:', event.data); 140 | const message = event.data; 141 | 142 | // Remove 'switchSidebarSide' handling as side is fixed 143 | // if (message && message.action === 'switchSidebarSide') { ... } 144 | 145 | if (message && message.action === 'toggleSidebarVisibility') { 146 | isSidebarCollapsed = message.isCollapsed; 147 | console.log('[Content Script] Toggled visibility. Collapsed:', isSidebarCollapsed); 148 | 149 | // Width is now fixed, so no need to manage currentSidebarWidth or lastNonCollapsedWidth for resizing. 150 | // applySidebarStyling will use FIXED_PANEL_WIDTH or 0px. 151 | 152 | applySidebarStyling(); 153 | // updateResizerStyle(); // Resizer removed 154 | adjustWhatsAppLayout(); 155 | saveSidebarState(); 156 | iframe?.contentWindow?.postMessage({ action: 'updatePanelControls', side: 'right', isCollapsed: isSidebarCollapsed }, '*'); // Side is fixed 157 | 158 | } else if (message && message.type === 'SEND_TO_BACKGROUND') { 159 | console.log('[AI Co-Pilot] Received SEND_TO_BACKGROUND from iframe:', message.message); 160 | chrome.runtime.sendMessage(message.message, (response) => { 161 | const error = chrome.runtime.lastError; 162 | if (error) { 163 | console.warn('[AI Co-Pilot] Error sending message to background:', error); 164 | if (iframe && iframe.contentWindow) { 165 | iframe.contentWindow.postMessage({ 166 | type: 'BACKGROUND_RESPONSE', 167 | error: error.message, 168 | originalRequest: message.message 169 | }, '*'); 170 | } 171 | return; 172 | } 173 | console.log('[AI Co-Pilot] Relaying BACKGROUND_RESPONSE to iframe:', response); 174 | if (iframe && iframe.contentWindow) { 175 | iframe.contentWindow.postMessage({ 176 | type: 'BACKGROUND_RESPONSE', 177 | response: response, 178 | originalRequest: message.message 179 | }, '*'); 180 | } 181 | }); 182 | } else if (message && message.type) { 183 | // Relay other message types from iframe to background 184 | console.log('[AI Co-Pilot] Relaying other message type from iframe to background:', message); 185 | chrome.runtime.sendMessage(message, (response) => { 186 | const error = chrome.runtime.lastError; 187 | if (error) { 188 | console.warn('[AI Co-Pilot] Error sending relayed message to background:', error); 189 | if (iframe && iframe.contentWindow) { 190 | iframe.contentWindow.postMessage({ type: 'BACKGROUND_RELAY_ERROR', error: error.message, originalRequest: message }, '*'); 191 | } 192 | return; 193 | } 194 | if (iframe && iframe.contentWindow && response) { 195 | iframe.contentWindow.postMessage({ type: 'BACKGROUND_RELAY_RESPONSE', response: response, originalRequest: message }, '*'); 196 | } 197 | }); 198 | } 199 | } catch (e) { 200 | console.error('Error processing message from iframe:', e); 201 | } 202 | } 203 | }); 204 | 205 | // Add a chrome.runtime.onMessage listener that logs and relays to iframe 206 | // THIS LISTENER IS NOW INSIDE injectSidebar TO HAVE ACCESS TO SCOPED VARIABLES 207 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 208 | console.log('[AI Co-Pilot] Content script (within injectSidebar) received message from background/action:', message); 209 | 210 | if (message.type === 'TOGGLE_PANEL_VISIBILITY_FROM_ACTION') { 211 | if (!sidebarInjected) { // Should not happen if this listener is inside injectSidebar and injectSidebar completed 212 | console.warn('[Content Script] Received TOGGLE_PANEL_VISIBILITY_FROM_ACTION but sidebar not injected. This is unexpected.'); 213 | sendResponse({ success: false, error: 'Sidebar not injected, cannot toggle from action.' }); 214 | return true; 215 | } 216 | 217 | console.log('[Content Script] Handling TOGGLE_PANEL_VISIBILITY_FROM_ACTION. Current collapsed state:', isSidebarCollapsed); 218 | 219 | isSidebarCollapsed = !isSidebarCollapsed; // Toggle the collapsed state 220 | console.log('[Content Script] New collapsed state:', isSidebarCollapsed); 221 | 222 | // Width is fixed, applySidebarStyling handles the visual width (0px or FIXED_PANEL_WIDTH) 223 | applySidebarStyling(); 224 | // updateResizerStyle(); // Resizer removed 225 | adjustWhatsAppLayout(); 226 | saveSidebarState(); 227 | 228 | const panelIframe = document.getElementById('whatsapp-ai-copilot-iframe'); // iframe variable is in this scope 229 | if (panelIframe && panelIframe.contentWindow) { 230 | panelIframe.contentWindow.postMessage({ action: 'updatePanelControls', side: 'right', isCollapsed: isSidebarCollapsed }, '*'); 231 | } 232 | sendResponse({ success: true, newCollapsedState: isSidebarCollapsed }); 233 | return true; 234 | 235 | } else { 236 | // Relay other messages from background to iframe (if any) 237 | // Note: The 'iframe' variable here refers to the one defined at the top of injectSidebar 238 | if (iframe && iframe.contentWindow) { 239 | console.log('[AI Co-Pilot] Relaying other message from background to iframe (within injectSidebar):', message); 240 | iframe.contentWindow.postMessage(message, '*'); 241 | } 242 | sendResponse({ ack: true, note: "Message relayed or unhandled by content script specific logic (within injectSidebar)." }); 243 | } 244 | // Ensure a consistent return value for the listener if not returning true for async 245 | return false; 246 | }); 247 | } 248 | 249 | // Ensure injection happens after the page is fully loaded 250 | // or when WhatsApp Web dynamically loads its UI. 251 | 252 | // Simple approach: try to inject when the script runs and on DOMContentLoaded 253 | if (document.readyState === "complete" || document.readyState === "interactive") { 254 | injectSidebar(); 255 | } else { 256 | document.addEventListener("DOMContentLoaded", injectSidebar); 257 | } 258 | 259 | // Add MutationObserver for more reliable injection 260 | const PANE_SIDE_SELECTOR = '#pane-side'; // More specific selector for the chat list pane 261 | 262 | const appObserver = new MutationObserver((mutations, observer) => { 263 | if (document.querySelector(PANE_SIDE_SELECTOR) && !document.getElementById(SIDEBAR_ID)) { 264 | console.log("WhatsApp main chat UI detected (#pane-side), injecting sidebar..."); 265 | injectSidebar(); 266 | 267 | // Attempt to disconnect observer after successful injection, 268 | // but with a small delay to ensure everything is settled. 269 | setTimeout(() => { 270 | if (document.getElementById(SIDEBAR_ID)) { 271 | console.log("Sidebar injected, disconnecting observer."); 272 | observer.disconnect(); 273 | } else { 274 | console.warn("Sidebar injection might have failed after #pane-side detection."); 275 | // Optionally, try one more time or log an error 276 | // injectSidebar(); 277 | } 278 | }, 500); // Increased delay for stability 279 | } 280 | }); 281 | 282 | // Start observing the document body for when WhatsApp Web loads its UI 283 | // Observe for changes deeper in the tree as #pane-side might be added later. 284 | appObserver.observe(document.documentElement, { childList: true, subtree: true }); 285 | 286 | 287 | // Fallback/Retry mechanism using the more specific selector 288 | const injectionInterval = setInterval(() => { 289 | if (document.body && !document.getElementById(SIDEBAR_ID) && document.querySelector(PANE_SIDE_SELECTOR)) { 290 | console.log("WhatsApp #pane-side element found via interval, attempting to inject sidebar."); 291 | injectSidebar(); 292 | } 293 | // Clear interval if sidebar is found, regardless of how it was injected 294 | if(document.getElementById(SIDEBAR_ID)){ 295 | console.log("Sidebar found, clearing injection interval."); 296 | clearInterval(injectionInterval); 297 | // Also disconnect observer if it's still running and sidebar is now present 298 | if (appObserver) appObserver.disconnect(); 299 | } 300 | }, 1500); // Try every 1.5 seconds 301 | 302 | // Clear interval and observer after some time to prevent indefinite running 303 | setTimeout(() => { 304 | if (injectionInterval) clearInterval(injectionInterval); 305 | if (appObserver) appObserver.disconnect(); 306 | console.log("Stopped injection attempts after timeout."); 307 | }, 30000); // Stop after 30 seconds 308 | 309 | 310 | // Notify background that content script is ready (for robust handshake) 311 | try { 312 | chrome.runtime.sendMessage({ type: 'CONTENT_SCRIPT_READY' }, () => { 313 | if (chrome.runtime.lastError) { 314 | console.warn('[AI Co-Pilot] Error sending CONTENT_SCRIPT_READY:', chrome.runtime.lastError.message); 315 | } else { 316 | console.log('[AI Co-Pilot] Sent CONTENT_SCRIPT_READY to background'); 317 | } 318 | }); 319 | } catch (e) { 320 | console.error('[AI Co-Pilot] Exception sending CONTENT_SCRIPT_READY:', e); 321 | } 322 | 323 | // Add a chrome.runtime.onMessage listener that logs and relays to iframe 324 | -------------------------------------------------------------------------------- /backend/src/ai.service.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import dotenv from 'dotenv'; 3 | import { Message } from 'whatsapp-web.js'; // Assuming Message type is needed for context 4 | 5 | // Load environment variables 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY?.trim(), 10 | }); 11 | 12 | if (!process.env.OPENAI_API_KEY) { 13 | console.warn("OPENAI_API_KEY is not set. AI features will not work."); 14 | } 15 | 16 | interface MessageForAI { 17 | role: 'system' | 'user' | 'assistant'; 18 | content: string; 19 | } 20 | 21 | /** 22 | * Formats an array of WhatsApp messages into a single string for AI processing. 23 | * Prepends "Conversation History:" and lists messages with sender and body. 24 | * Sorts messages by timestamp. 25 | * @param {Message[]} messages - An array of `whatsapp-web.js` Message objects. 26 | * @returns {Promise} A string representing the formatted conversation history. 27 | */ 28 | const formatMessagesForAI = async (messages: Message[]): Promise => { 29 | let conversationContext = "Conversation History:\n"; 30 | // Sort messages by timestamp if not already sorted 31 | const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp); 32 | 33 | for (const msg of sortedMessages) { 34 | const sender = msg.fromMe ? 'You' : (msg.author || msg.from); // msg.author for group messages 35 | let contactName = sender; 36 | // Fetching contact can be slow and error-prone, especially in a batch. 37 | // For MVP, we might simplify this or make it optional. 38 | // if (!msg.fromMe && msg.getContact) { 39 | // try { 40 | // const contact = await msg.getContact(); 41 | // contactName = contact.pushname || contact.name || sender; 42 | // } catch (e) { 43 | // // console.warn(`Could not get contact for message from ${sender}`); 44 | // contactName = sender; // Fallback 45 | // } 46 | // } 47 | conversationContext += `${contactName}: ${msg.body}\n`; 48 | } 49 | return conversationContext; 50 | }; 51 | 52 | /** 53 | * Generates a summary for a given array of WhatsApp messages using the OpenAI API. 54 | * @param {Message[]} messages - An array of `whatsapp-web.js` Message objects to summarize. 55 | * @param {string} [startDate] - Optional start date of the message range (DD/MM/YYYY). 56 | * @param {string} [endDate] - Optional end date of the message range (DD/MM/YYYY). 57 | * @param {string} [customPromptText] - Optional custom system prompt text. 58 | * @param {string} [openaiApiKey] - Optional user-provided OpenAI API key. 59 | * @returns {Promise} A promise that resolves to the AI-generated summary, or an error message. 60 | */ 61 | export const generateSummary = async ( 62 | messages: Message[], 63 | startDate?: string, 64 | endDate?: string, 65 | customPromptText?: string, 66 | openaiApiKey?: string 67 | ): Promise => { 68 | const activeOpenAI = (openaiApiKey && openaiApiKey.trim() !== "") 69 | ? new OpenAI({ apiKey: openaiApiKey.trim() }) 70 | : openai; 71 | 72 | if (!activeOpenAI.apiKey) { // Check the potentially new client's API key 73 | return "AI service is not configured (OpenAI API key missing or invalid)."; 74 | } 75 | if (!messages || messages.length === 0) { 76 | return "No messages provided for summarization."; 77 | } 78 | 79 | try { 80 | const conversationContext = await formatMessagesForAI(messages); 81 | 82 | let dateRangeInfo = ""; 83 | if (startDate && endDate) { 84 | dateRangeInfo = ` for the period ${startDate} to ${endDate}`; 85 | } else if (startDate) { 86 | dateRangeInfo = ` starting from ${startDate}`; 87 | } else if (endDate) { 88 | dateRangeInfo = ` up to ${endDate}`; 89 | } 90 | 91 | const numMessages = messages.length; 92 | let summaryDetailLevel = "standard"; 93 | let maxTokensForSummary = 2500; // Default 94 | 95 | if (numMessages > 1500) { 96 | summaryDetailLevel = "very detailed and comprehensive, covering as many distinct discussion threads and nuances as possible"; 97 | maxTokensForSummary = 3500; // Push higher for very large message sets 98 | } else if (numMessages > 500) { 99 | summaryDetailLevel = "detailed, covering major topics and important sub-points thoroughly"; 100 | maxTokensForSummary = 3000; 101 | } else if (numMessages < 100 && numMessages > 0) { 102 | summaryDetailLevel = "concise but informative, highlighting the absolute key takeaways"; 103 | maxTokensForSummary = 1500; 104 | } 105 | // Ensure maxTokensForSummary does not exceed model limits (e.g. 4096 for gpt-4o-mini output) 106 | maxTokensForSummary = Math.min(maxTokensForSummary, 4000); 107 | 108 | 109 | const DEFAULT_SYSTEM_PROMPT_FOR_SUMMARY = `You are an AI specializing in summarizing WhatsApp group conversations, particularly for active, high-volume technical groups like "AI Israel" where discussions revolve heavily around AI concepts, tools, news, and problem-solving. Your primary goal is to distill potentially hundreds or thousands of messages into a clear, actionable, and appropriately ${summaryDetailLevel} HTML summary. 110 | The current summary request covers ${numMessages} messages${dateRangeInfo}. Adjust the depth and breadth of your summary accordingly. It is better to provide more detail and be comprehensive than to be too brief, especially for larger message sets. Capture the essence and key information effectively. 111 | 112 | **CRITICAL OUTPUT INSTRUCTION: Your ENTIRE response MUST be a single block of valid HTML. Do NOT include any markdown, code block specifiers (like \`\`\`html), or any text outside of the HTML structure itself.** 113 | 114 | **LANGUAGE INSTRUCTION:** 115 | * If the conversation history is predominantly **Hebrew**, your entire HTML response (all text content, titles, etc.) MUST be in **HEBREW**. 116 | * Otherwise, if another non-English language is clearly predominant, respond in that language. 117 | * If English is primary or no other language is clearly dominant, respond in English. 118 | 119 | **HTML STRUCTURE & STYLING GUIDELINES (Same as before - ensure adherence):** 120 | * **Main Container:** Wrap your entire summary in a single \`
\`. 121 | * **Section Titles:** Use \`

\` for main section titles. Each \`

\` MUST start with a relevant emoji. 122 | * Example (Hebrew): \`

📊 סקירה כללית:

\` 123 | * Example (English): \`

📊 Main Overview:

\` 124 | * **Paragraphs:** Use \`

\` for all descriptive text. 125 | * **Emphasis:** Use \`\` for bolding key terms, names, or takeaways. Use \`\` for italics if needed for nuance. 126 | * **Lists:** Use \`

    \` for unordered lists and \`
  • \` for list items. List items should generally contain a \`

    \` tag for their content for consistent spacing. 127 | * **Hyperlinks:** 128 | * **MUST be HTML anchor tags:** \`Descriptive Link Text\`. 129 | * \`target="_blank"\` is crucial. 130 | * Link text MUST be descriptive (e.g., "Research Paper on LLMs," not just the URL). 131 | * **Comprehensiveness & Detail:** For larger message sets or longer date ranges, expand the "Key Topics & Insights" section to cover more distinct topics (e.g., 5-7 instead of 3-5 if warranted) and provide more detail within each. Similarly, the "Notable Links" and "Key Questions" sections can be more extensive. The goal is to transfer the full message of the group discussions effectively. 132 | 133 | **SUMMARY STRUCTURE (Follow this order precisely, adapt detail based on input size):** 134 | 135 | \`\`\`html 136 |

    137 |

    📊 Main Overview (סקירה כללית):

    138 |

    Provide a high-level summary (2-4 sentences, more if many messages). What were the absolute main themes and the general conversational drift in this segment of messages${dateRangeInfo}? Focus on the essence.

    139 | 140 |

    💡 Key Topics & Insights (נושאים ותובנות עיקריים):

    141 |

    This is the core of your summary. Identify the most significant, distinct topics discussed. For ${numMessages > 500 ? 'a larger set of messages like this, aim for 5-7 key topics if distinct threads exist' : 'this set of messages, aim for 3-5 key topics'}. For each topic:

    142 |
      143 |
    • 144 |

      Topic Name 1: Explain the core discussion points, new information shared (e.g., new AI models, tools, techniques), or main questions/debates in more detail if the message volume is high. Mention key insights or conclusions if any were reached.

      145 |
    • 146 | 147 |
    148 | 149 |

    🔗 Notable Links & Resources (קישורים ומשאבים חשובים):

    150 |

    List important links, articles, tools, GitHub repositories, or other resources shared. Be more inclusive if many resources were shared over a longer period.

    151 | 155 | 156 |

    ❓ Key Questions Asked (שאלות מרכזיות שנשאלו):

    157 |

    List significant or recurring questions. For larger message sets, include more questions if they represent distinct areas of inquiry.

    158 |
      159 |
    • Question: "Actual question text?" - Briefly note if it was answered, or if it remains open.

    • 160 | 161 |
    162 | 163 |

    🔥 Hot Topic Snippet (נושא חם בקצרה):

    164 |

    From the key topics, identify the single most actively discussed, debated, or "hottest" thread. Provide a 2-3 sentence highlight capturing its essence, potentially more if it was a very dominant topic.

    165 | 166 |

    🗣️ Dive Deeper? (רוצים לצלול פנימה?):

    167 |

    Is there a specific topic or question from this summary you'd like to explore in more detail?

    168 |
    169 | \`\`\` 170 | 171 | **Final Check:** Before outputting, ensure your response is ONLY the HTML content as specified, starting with \`
    \` and ending with \`
    \`. No extra text, no markdown, no code fences. 172 | `; 173 | 174 | let systemPromptToUse = DEFAULT_SYSTEM_PROMPT_FOR_SUMMARY; 175 | if (customPromptText && customPromptText.trim() !== "") { 176 | systemPromptToUse = `${customPromptText.trim()}\n\nIMPORTANT: The following is the primary set of instructions you must adhere to:\n\n${DEFAULT_SYSTEM_PROMPT_FOR_SUMMARY}`; 177 | console.log(`[AI.SERVICE] Augmenting default summary prompt with custom instruction: "${customPromptText.substring(0, 100)}..."`); 178 | } else { 179 | console.log(`[AI.SERVICE] Using default system prompt for summary.`); 180 | } 181 | 182 | if (openaiApiKey && openaiApiKey.trim() !== "") { 183 | console.log(`[AI.SERVICE] Using user-provided OpenAI API key for summary.`); 184 | } else { 185 | console.log(`[AI.SERVICE] Using default backend OpenAI API key for summary.`); 186 | } 187 | 188 | const userQuery = `${conversationContext}\n\nTask: Summarize the above conversation (which includes ${numMessages} messages${dateRangeInfo}) according to the detailed system instructions. Prioritize comprehensiveness and detail appropriate for the volume of messages, ensuring the summary is not too short. Pay close attention to the language instruction.`; 189 | 190 | const formattedMessagesForOpenAI: MessageForAI[] = [ 191 | { role: 'system', content: systemPromptToUse }, 192 | { role: 'user', content: userQuery } 193 | ]; 194 | 195 | const aiResponse = await activeOpenAI.chat.completions.create({ 196 | model: process.env.OPENAI_MODEL || 'gpt-4o-mini', 197 | messages: formattedMessagesForOpenAI, 198 | max_tokens: maxTokensForSummary, 199 | temperature: 0.5, 200 | }); 201 | 202 | return aiResponse.choices[0].message.content || 'Sorry, I could not generate a summary.'; 203 | } catch (error: any) { 204 | console.error('Error generating summary with AI:', error); 205 | if (error.response && error.response.data && error.response.data.error) { 206 | return `AI API Error: ${error.response.data.error.message}`; 207 | } 208 | return 'An error occurred while generating the summary. Please try again later.'; 209 | } 210 | }; 211 | 212 | /** 213 | * Answers a question based on a given array of WhatsApp messages and a question string, using the OpenAI API. 214 | * @param {Message[]} messages - An array of `whatsapp-web.js` Message objects providing context. 215 | * @param {string} question - The question to answer. 216 | * @param {string} [customPromptText] - Optional custom system prompt text. 217 | * @param {string} [openaiApiKey] - Optional user-provided OpenAI API key. 218 | * @returns {Promise} A promise that resolves to the AI-generated answer, or an error message. 219 | */ 220 | export const answerQuestion = async ( 221 | messages: Message[], 222 | question: string, 223 | customPromptText?: string, 224 | openaiApiKey?: string 225 | ): Promise => { 226 | const activeOpenAI = (openaiApiKey && openaiApiKey.trim() !== "") 227 | ? new OpenAI({ apiKey: openaiApiKey.trim() }) 228 | : openai; 229 | 230 | if (!activeOpenAI.apiKey) { // Check the potentially new client's API key 231 | return "AI service is not configured (OpenAI API key missing or invalid)."; 232 | } 233 | if (!messages || messages.length === 0) { 234 | return "No message context provided to answer the question."; 235 | } 236 | if (!question || question.trim() === "") { 237 | return "No question provided."; 238 | } 239 | 240 | try { 241 | const conversationContext = await formatMessagesForAI(messages); 242 | const DEFAULT_SYSTEM_PROMPT_FOR_QA = `You are an AI assistant designed to answer questions about a specific WhatsApp group conversation, often focused on technology or AI discussions. Use ONLY the provided message history to answer the user's question accurately and concisely, extracting relevant information. 243 | 244 | **LANGUAGE INSTRUCTION:** 245 | * **If the conversation history (and potentially the user's question) contains a significant amount of Hebrew, your answer MUST be in HEBREW.** 246 | * Otherwise, if another non-English language is clearly predominant in the conversation, respond in that language. 247 | * If the conversation is primarily English or a mix of languages without a clear non-English predominance, respond in English. 248 | 249 | If the answer to the question cannot be found within the provided messages, clearly state that the information is not available in the provided context (in the target language). Do not make assumptions or provide information from outside the conversation.`; 250 | 251 | let systemPromptToUse = DEFAULT_SYSTEM_PROMPT_FOR_QA; 252 | if (customPromptText && customPromptText.trim() !== "") { 253 | systemPromptToUse = `${customPromptText.trim()}\n\nIMPORTANT: The following is the primary set of instructions you must adhere to:\n\n${DEFAULT_SYSTEM_PROMPT_FOR_QA}`; 254 | console.log(`[AI.SERVICE] Augmenting default Q&A prompt with custom instruction: "${customPromptText.substring(0, 100)}..."`); 255 | } else { 256 | console.log(`[AI.SERVICE] Using default system prompt for Q&A.`); 257 | } 258 | 259 | if (openaiApiKey && openaiApiKey.trim() !== "") { 260 | console.log(`[AI.SERVICE] Using user-provided OpenAI API key for Q&A.`); 261 | } else { 262 | console.log(`[AI.SERVICE] Using default backend OpenAI API key for Q&A.`); 263 | } 264 | 265 | const userQuery = `${conversationContext}\n\nUser's Question: ${question}\n\nTask: Answer the user's question based ONLY on the conversation history provided above, paying close attention to the language instruction.`; 266 | 267 | const formattedMessagesForOpenAI: MessageForAI[] = [ 268 | { role: 'system', content: systemPromptToUse }, 269 | { role: 'user', content: userQuery } 270 | ]; 271 | 272 | const aiResponse = await activeOpenAI.chat.completions.create({ 273 | model: process.env.OPENAI_MODEL || 'gpt-4o-mini', // Ensure this uses the intended model 274 | messages: formattedMessagesForOpenAI, 275 | max_tokens: 250, // Slightly increased if answers might need more detail 276 | temperature: 0.3, // Lower temperature for more factual, context-bound answers 277 | }); 278 | 279 | return aiResponse.choices[0].message.content || 'Sorry, I could not generate an answer.'; 280 | } catch (error: any) { 281 | console.error('Error answering question with AI:', error); 282 | if (error.response && error.response.data && error.response.data.error) { 283 | return `AI API Error: ${error.response.data.error.message}`; 284 | } 285 | return 'An error occurred while generating the answer. Please try again later.'; 286 | } 287 | }; -------------------------------------------------------------------------------- /extension/panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WhatsApp AI Co-Pilot - Sidebar Interactions 3 | */ 4 | 5 | // This script will be injected by content.js and will handle sidebar interactions 6 | 7 | // Sidebar animation variables 8 | let sidebarOpen = true; 9 | const BACKEND_URL = 'http://localhost:3000'; 10 | let currentSelectedChatId = null; // Stores the ID of the currently selected chat 11 | let currentSelectedChatName = null; // Stores the name of the currently selected chat 12 | let fetchedMessagesForSelectedChat = []; // Stores messages for AI processing (role might change) 13 | 14 | const DEFAULT_SUMMARY_PROMPT_TEXT_DISPLAY = `The default summary prompt is dynamically generated by the backend based on message count and date range. It instructs the AI to produce a detailed HTML summary, adapting to the conversation's primary language (e.g., Hebrew or English), and focusing on key topics, insights, notable links, and questions. The structure includes sections like Main Overview, Key Topics, Notable Links, Key Questions, and a Hot Topic Snippet.`; 15 | const DEFAULT_QA_PROMPT_TEXT_DISPLAY = `You are an AI assistant designed to answer questions about a specific WhatsApp group conversation, often focused on technology or AI discussions. Use ONLY the provided message history to answer the user's question accurately and concisely, extracting relevant information. 16 | 17 | **LANGUAGE INSTRUCTION:** 18 | * If the conversation history (and potentially the user's question) contains a significant amount of Hebrew, your answer MUST be in HEBREW. 19 | * Otherwise, if another non-English language is clearly predominant in the conversation, respond in that language. 20 | * If the conversation is primarily English or a mix of languages without a clear non-English predominance, respond in English. 21 | 22 | If the answer to the question cannot be found within the provided messages, clearly state that the information is not available in the provided context (in the target language). Do not make assumptions or provide information from outside the conversation.`; 23 | 24 | // Date filter elements 25 | let startDateInput, endDateInput, clearDatesButton, dateFilterAreaEl, fetchUnreadCheckbox, filterOptionsLabelEl, unreadFilterAreaEl; 26 | 27 | // DOM element references 28 | let chatSelectorDropdown, whatsappStatusArea, whatsappConnectionStatus, whatsappQrCodeArea, whatsappQrCodeImg, chatSelectorArea, summarizeButton, askQuestionButton, aiQueryInput, aiChatMessagesDiv, foldButtonElement; 29 | let settingsButton, customPromptSettingsSection, customPromptTextarea, saveCustomPromptButton, resetCustomPromptButton, viewDefaultPromptsButton, defaultPromptsDisplayArea, defaultSummaryPromptDisplay, defaultQaPromptDisplay; 30 | let openaiApiKeyInput, saveOpenaiApiKeyButton; // New elements for OpenAI API Key 31 | 32 | // Wait for DOM to be ready 33 | document.addEventListener('DOMContentLoaded', () => { 34 | // Get DOM elements 35 | chatSelectorDropdown = document.getElementById('chat-selector'); 36 | whatsappStatusArea = document.getElementById('whatsapp-status-area'); 37 | whatsappConnectionStatus = document.getElementById('whatsapp-connection-status'); 38 | whatsappQrCodeArea = document.getElementById('whatsapp-qr-code-area'); 39 | whatsappQrCodeImg = document.getElementById('whatsapp-qr-code-img'); // This is a div now 40 | chatSelectorArea = document.getElementById('chat-selector-area'); 41 | 42 | summarizeButton = document.getElementById('summarize-group-button'); 43 | askQuestionButton = document.getElementById('ask-question-button'); 44 | aiQueryInput = document.getElementById('ai-query-input'); 45 | aiChatMessagesDiv = document.getElementById('ai-chat-messages'); 46 | foldButtonElement = document.getElementById('fold-button'); 47 | 48 | // NEW: Get date filter elements 49 | startDateInput = document.getElementById('startDate'); 50 | endDateInput = document.getElementById('endDate'); 51 | clearDatesButton = document.getElementById('clear-dates-button'); 52 | dateFilterAreaEl = document.getElementById('date-filter-area'); 53 | fetchUnreadCheckbox = document.getElementById('fetch-unread-checkbox'); 54 | filterOptionsLabelEl = document.getElementById('filter-options-label'); 55 | unreadFilterAreaEl = document.getElementById('unread-filter-area'); 56 | 57 | // Custom Prompt Settings Elements 58 | settingsButton = document.getElementById('settings-button'); 59 | customPromptSettingsSection = document.getElementById('custom-prompt-settings-section'); 60 | customPromptTextarea = document.getElementById('custom-prompt-textarea'); 61 | saveCustomPromptButton = document.getElementById('save-custom-prompt-button'); 62 | resetCustomPromptButton = document.getElementById('reset-custom-prompt-button'); 63 | viewDefaultPromptsButton = document.getElementById('view-default-prompts-button'); 64 | defaultPromptsDisplayArea = document.getElementById('default-prompts-display-area'); 65 | defaultSummaryPromptDisplay = document.getElementById('default-summary-prompt-display'); 66 | defaultQaPromptDisplay = document.getElementById('default-qa-prompt-display'); 67 | 68 | // OpenAI API Key Elements 69 | openaiApiKeyInput = document.getElementById('openai-api-key-input'); 70 | saveOpenaiApiKeyButton = document.getElementById('save-openai-api-key-button'); 71 | 72 | if (clearDatesButton) { 73 | clearDatesButton.addEventListener('click', () => { 74 | if (startDateInput) startDateInput.value = ''; 75 | if (endDateInput) endDateInput.value = ''; 76 | }); 77 | } 78 | 79 | setupCustomPromptControls(); 80 | setupOpenApiKeyControls(); // New function call 81 | waitForSidebarAndSetup(); 82 | 83 | function updateFoldButtonIcon(isCollapsed) { 84 | if (foldButtonElement) { 85 | const icon = foldButtonElement.querySelector('i'); 86 | if (isCollapsed) { 87 | icon.className = 'fas fa-chevron-left'; 88 | } else { 89 | icon.className = 'fas fa-chevron-right'; 90 | } 91 | } 92 | } 93 | 94 | function handleToggleSidebarVisibility() { 95 | sidebarOpen = !sidebarOpen; 96 | window.parent.postMessage({ action: 'toggleSidebarVisibility', isCollapsed: !sidebarOpen }, '*'); 97 | } 98 | 99 | if (foldButtonElement) { 100 | foldButtonElement.addEventListener('click', (e) => { 101 | e.preventDefault(); 102 | e.stopPropagation(); 103 | handleToggleSidebarVisibility(); 104 | }); 105 | } 106 | 107 | window.addEventListener('message', (event) => { 108 | if (event.source !== window.parent) { 109 | return; 110 | } 111 | const message = event.data; 112 | if (message && message.action === 'updatePanelControls') { 113 | let isSidebarCollapsed = message.isCollapsed; 114 | sidebarOpen = !isSidebarCollapsed; 115 | updateFoldButtonIcon(isSidebarCollapsed); 116 | } 117 | }); 118 | 119 | }); 120 | 121 | function updateWhatsappConnectionStatus(message, isError = false) { 122 | if (whatsappConnectionStatus) { 123 | whatsappConnectionStatus.textContent = message; 124 | whatsappConnectionStatus.className = isError ? 'status-error' : 'status-ok'; 125 | } 126 | 127 | const elementsToHideOnError = [ 128 | chatSelectorArea, summarizeButton, dateFilterAreaEl, 129 | filterOptionsLabelEl, unreadFilterAreaEl, document.querySelector('.chat-input-area') 130 | ]; 131 | 132 | if (isError) { 133 | elementsToHideOnError.forEach(el => { if (el) el.style.display = 'none'; }); 134 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'block'; 135 | } 136 | } 137 | 138 | async function fetchAndDisplayQrCode() { 139 | if (!whatsappQrCodeImg || !whatsappQrCodeArea || !whatsappConnectionStatus) { 140 | console.error("QR display elements not found."); 141 | return; 142 | } 143 | try { 144 | updateWhatsappConnectionStatus('Fetching QR code...'); 145 | const response = await fetch(`${BACKEND_URL}/auth/qr`); 146 | if (!response.ok) { 147 | const errorData = await response.json().catch(() => ({ error: 'Failed to fetch QR code details.' })); 148 | throw new Error(errorData.error || `HTTP error ${response.status}`); 149 | } 150 | const data = await response.json(); 151 | if (data.qrCode) { 152 | if (typeof QRCode !== 'undefined') { 153 | whatsappQrCodeImg.innerHTML = ''; 154 | new QRCode(whatsappQrCodeImg, { 155 | text: data.qrCode, width: 180, height: 180, 156 | colorDark: "#000000", colorLight: "#ffffff", 157 | correctLevel: QRCode.CorrectLevel.H 158 | }); 159 | updateWhatsappConnectionStatus('Scan QR code with WhatsApp.'); 160 | whatsappQrCodeArea.style.display = 'block'; 161 | const elementsToHide = [ 162 | chatSelectorArea, summarizeButton, dateFilterAreaEl, 163 | filterOptionsLabelEl, unreadFilterAreaEl, document.querySelector('.chat-input-area') 164 | ]; 165 | elementsToHide.forEach(el => { if (el) el.style.display = 'none'; }); 166 | } else { 167 | whatsappQrCodeImg.innerHTML = 'QR Library not loaded.'; 168 | updateWhatsappConnectionStatus('QR library missing. Check console.', true); 169 | console.error("QRCode library is not loaded."); 170 | } 171 | } else { 172 | updateWhatsappConnectionStatus('QR code not available from backend.', false); 173 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 174 | } 175 | } catch (error) { 176 | console.error('Error fetching or displaying QR code:', error); 177 | updateWhatsappConnectionStatus(`Error fetching QR: ${error.message}`, true); 178 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 179 | } 180 | } 181 | 182 | async function fetchGroups() { 183 | if (!isBackendConnected()) { 184 | updateWhatsappConnectionStatus('Backend not connected. Cannot fetch groups.', true); 185 | return; 186 | } 187 | try { 188 | updateWhatsappConnectionStatus('Fetching group list...'); 189 | const response = await fetch(`${BACKEND_URL}/groups`); 190 | if (!response.ok) { 191 | let errorText = `HTTP error ${response.status}`; 192 | try { const errorData = await response.json(); errorText = errorData.error || errorText; } catch (e) { /* ignore */ } 193 | throw new Error(errorText); 194 | } 195 | const groups = await response.json(); 196 | populateChatSelector(groups); 197 | if (groups && Array.isArray(groups) && groups.length > 0) { 198 | updateWhatsappConnectionStatus('WhatsApp Connected. Select a group.'); 199 | if(chatSelectorArea) chatSelectorArea.style.display = 'block'; 200 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 201 | } else { 202 | updateWhatsappConnectionStatus('WhatsApp Connected. No groups found or accessible.'); 203 | if(chatSelectorArea) chatSelectorArea.style.display = 'block'; 204 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 205 | } 206 | hideInitialLoadingIndicator(); 207 | } catch (error) { 208 | const errorMessage = error instanceof Error ? error.message : String(error); 209 | updateWhatsappConnectionStatus(`Error fetching groups: ${errorMessage}`, true); 210 | populateChatSelector([]); 211 | hideInitialLoadingIndicator(); 212 | } 213 | } 214 | 215 | function populateChatSelector(groups) { 216 | if (!chatSelectorDropdown) return; 217 | const previouslySelectedId = currentSelectedChatId; 218 | chatSelectorDropdown.innerHTML = ''; 219 | const defaultOption = document.createElement('option'); 220 | defaultOption.value = ""; 221 | defaultOption.textContent = groups && groups.length > 0 ? "-- Select a Group --" : "-- No Groups Available --"; 222 | chatSelectorDropdown.appendChild(defaultOption); 223 | 224 | if (groups && Array.isArray(groups)) { 225 | groups.forEach(group => { 226 | if (group && group.id && group.name) { 227 | const option = document.createElement('option'); 228 | option.value = group.id; 229 | option.textContent = group.name; 230 | chatSelectorDropdown.appendChild(option); 231 | } 232 | }); 233 | if (previouslySelectedId) { 234 | chatSelectorDropdown.value = previouslySelectedId; 235 | if (chatSelectorDropdown.value !== previouslySelectedId) currentSelectedChatId = ""; 236 | } 237 | } 238 | } 239 | 240 | function waitForSidebarAndSetup() { 241 | const panel = document.getElementById('ai-copilot-panel'); 242 | if (!panel) { 243 | setTimeout(waitForSidebarAndSetup, 50); 244 | return; 245 | } 246 | showInitialLoadingIndicator(); 247 | setupChatInterface(); 248 | attachLoadingIndicators(); 249 | addRefreshButton(); 250 | initAuthAndGroupListing(); 251 | checkBackendAndWhatsAppStatus(); 252 | const messageList = document.querySelector('.message-list'); 253 | if (messageList && messageList.childElementCount === 0) { 254 | addMessageToChat('assistant', "Welcome! Connect to WhatsApp and select a group to begin."); 255 | } 256 | setInterval(checkBackendAndWhatsAppStatus, 15000); 257 | setTimeout(setupNewFeatureButtons, 100); 258 | loadCustomPrompt(); 259 | loadOpenAIApiKey(); // Load saved OpenAI API Key 260 | } 261 | 262 | function setupOpenApiKeyControls() { 263 | if (saveOpenaiApiKeyButton && openaiApiKeyInput) { 264 | saveOpenaiApiKeyButton.addEventListener('click', () => { 265 | const apiKey = openaiApiKeyInput.value.trim(); 266 | if (apiKey) { 267 | chrome.storage.local.set({ userOpenAIApiKey: apiKey }, () => { 268 | if (chrome.runtime.lastError) { 269 | console.error('Error saving OpenAI API Key:', chrome.runtime.lastError); 270 | showStatus('שגיאה בשמירת מפתח OpenAI API.', true); 271 | } else { 272 | console.log('OpenAI API Key saved.'); 273 | showStatus('מפתח OpenAI API נשמר!', false); 274 | } 275 | }); 276 | } else { 277 | // If user clears the input and saves, remove it from storage 278 | chrome.storage.local.remove('userOpenAIApiKey', () => { 279 | if (chrome.runtime.lastError) { 280 | console.error('Error removing OpenAI API Key:', chrome.runtime.lastError); 281 | showStatus('שגיאה בניקוי מפתח OpenAI API.', true); 282 | } else { 283 | console.log('OpenAI API Key removed from storage.'); 284 | showStatus('מפתח OpenAI API נמחק (הגדרות שרת ישמשו כברירת מחדל).', false); 285 | } 286 | }); 287 | } 288 | }); 289 | } 290 | } 291 | 292 | function loadOpenAIApiKey() { 293 | if (openaiApiKeyInput) { 294 | chrome.storage.local.get('userOpenAIApiKey', (result) => { 295 | if (chrome.runtime.lastError) { 296 | console.error('Error loading OpenAI API Key:', chrome.runtime.lastError); 297 | return; 298 | } 299 | if (result.userOpenAIApiKey) { 300 | openaiApiKeyInput.value = result.userOpenAIApiKey; 301 | } 302 | }); 303 | } 304 | } 305 | 306 | function setupCustomPromptControls() { 307 | if (settingsButton) { 308 | settingsButton.addEventListener('click', () => { 309 | if (customPromptSettingsSection) { 310 | customPromptSettingsSection.style.display = customPromptSettingsSection.style.display === 'none' ? 'block' : 'none'; 311 | } 312 | }); 313 | } 314 | 315 | if (saveCustomPromptButton) { 316 | saveCustomPromptButton.addEventListener('click', () => { 317 | const promptText = customPromptTextarea ? customPromptTextarea.value.trim() : ""; 318 | chrome.storage.local.set({ customSystemPrompt: promptText }, () => { 319 | if (chrome.runtime.lastError) { 320 | console.error('Error saving custom prompt:', chrome.runtime.lastError); 321 | showStatus('שגיאה בשמירת ההנחיה.', true); 322 | } else { 323 | console.log('Custom prompt saved.'); 324 | showStatus('הנחיה מותאמת אישית נשמרה!', false); 325 | } 326 | }); 327 | }); 328 | } 329 | 330 | if (resetCustomPromptButton) { 331 | resetCustomPromptButton.addEventListener('click', () => { 332 | chrome.storage.local.remove('customSystemPrompt', () => { 333 | if (chrome.runtime.lastError) { 334 | console.error('Error resetting custom prompt:', chrome.runtime.lastError); 335 | showStatus('שגיאה באיפוס ההנחיה.', true); 336 | } else { 337 | if (customPromptTextarea) customPromptTextarea.value = ''; 338 | console.log('Custom prompt reset.'); 339 | showStatus('הנחיה אופסה לברירת מחדל.', false); 340 | } 341 | }); 342 | }); 343 | } 344 | 345 | if (viewDefaultPromptsButton) { 346 | viewDefaultPromptsButton.addEventListener('click', () => { 347 | if (defaultSummaryPromptDisplay) defaultSummaryPromptDisplay.textContent = DEFAULT_SUMMARY_PROMPT_TEXT_DISPLAY; 348 | if (defaultQaPromptDisplay) defaultQaPromptDisplay.textContent = DEFAULT_QA_PROMPT_TEXT_DISPLAY; 349 | if (defaultPromptsDisplayArea) { 350 | defaultPromptsDisplayArea.style.display = defaultPromptsDisplayArea.style.display === 'none' ? 'block' : 'none'; 351 | } 352 | }); 353 | } 354 | } 355 | 356 | function loadCustomPrompt() { 357 | if (customPromptTextarea) { 358 | chrome.storage.local.get('customSystemPrompt', (result) => { 359 | if (chrome.runtime.lastError) { 360 | console.error('Error loading custom prompt:', chrome.runtime.lastError); 361 | return; 362 | } 363 | if (result.customSystemPrompt) { 364 | customPromptTextarea.value = result.customSystemPrompt; 365 | } 366 | }); 367 | } 368 | } 369 | 370 | function showAILoading(feature) { 371 | if (feature === 'summarize' && summarizeButton) { 372 | summarizeButton.disabled = true; 373 | summarizeButton.innerHTML = ` מסכם...`; 374 | } else if (feature === 'ask' && askQuestionButton) { 375 | askQuestionButton.disabled = true; 376 | if (aiQueryInput) aiQueryInput.disabled = true; 377 | } 378 | } 379 | 380 | function hideAILoading(feature) { 381 | const messagesAvailable = !!currentSelectedChatId; 382 | const chatInputArea = document.querySelector('.chat-input-area'); 383 | 384 | if (feature === 'summarize' && summarizeButton) { 385 | summarizeButton.disabled = !messagesAvailable; 386 | summarizeButton.innerHTML = ` סכם קבוצה`; 387 | if (summarizeButton) summarizeButton.style.display = currentSelectedChatId ? 'flex' : 'none'; 388 | } else if (feature === 'ask' && askQuestionButton) { 389 | askQuestionButton.disabled = !messagesAvailable; 390 | if (aiQueryInput) aiQueryInput.disabled = !messagesAvailable; 391 | } 392 | if (chatInputArea) chatInputArea.style.display = currentSelectedChatId ? 'flex' : 'none'; 393 | if(summarizeButton) { // Redundant check but safe 394 | summarizeButton.style.display = currentSelectedChatId ? 'flex' : 'none'; 395 | summarizeButton.disabled = !messagesAvailable; 396 | } 397 | } 398 | 399 | function isValidDdMmYyyy(dateString) { 400 | if (!dateString) return true; 401 | const regex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/; 402 | const parts = dateString.match(regex); 403 | if (!parts) return false; 404 | const day = parseInt(parts[1], 10); 405 | const month = parseInt(parts[2], 10); 406 | const year = parseInt(parts[3], 10); 407 | if (year < 1000 || year > 3000 || month === 0 || month > 12) return false; 408 | const monthLength = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 409 | if (year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0)) monthLength[1] = 29; 410 | return day > 0 && day <= monthLength[month - 1]; 411 | } 412 | 413 | function setupNewFeatureButtons() { 414 | if (!summarizeButton || !askQuestionButton || !aiQueryInput) { 415 | console.error('AI feature buttons or input not found in DOM.'); 416 | return; 417 | } 418 | hideAILoading('summarize'); 419 | hideAILoading('ask'); 420 | 421 | summarizeButton.addEventListener('click', async () => { 422 | if (!currentSelectedChatId) { 423 | addMessageToChat('assistant', 'יש לבחור קבוצה לפני בקשת סיכום.', true); 424 | return; 425 | } 426 | const startDate = startDateInput ? startDateInput.value.trim() : null; 427 | const endDate = endDateInput ? endDateInput.value.trim() : null; 428 | if ((startDate && !isValidDdMmYyyy(startDate)) || (endDate && !isValidDdMmYyyy(endDate))) { 429 | addMessageToChat('assistant', 'פורמט תאריך לא תקין. אנא השתמש בפורמט DD/MM/YYYY או השאר ריק.', true); 430 | return; 431 | } 432 | addMessageToChat('user', `מבקש סיכום עבור ${currentSelectedChatName || 'הקבוצה הנבחרת'}${startDate || endDate ? ` (תאריכים: ${startDate || ''} - ${endDate || ''})` : ''}.`); 433 | showAILoading('summarize'); 434 | chrome.storage.local.get(['customSystemPrompt', 'userOpenAIApiKey'], async (storageResult) => { 435 | if (chrome.runtime.lastError) { 436 | console.error('Error getting data from storage for summary:', chrome.runtime.lastError); 437 | addMessageToChat('assistant', 'שגיאה בטעינת הגדרות.', true); 438 | hideAILoading('summarize'); 439 | return; 440 | } 441 | const customPromptText = storageResult.customSystemPrompt || null; 442 | const userApiKey = storageResult.userOpenAIApiKey || null; 443 | 444 | let logMsg = "[PANEL.JS] For summary: "; 445 | logMsg += customPromptText ? "Sending custom prompt. " : "No custom prompt. "; 446 | logMsg += userApiKey ? "Sending user API key." : "No user API key."; 447 | console.log(logMsg); 448 | 449 | try { 450 | const requestBody = { 451 | chatId: currentSelectedChatId, 452 | startDate: startDate || null, 453 | endDate: endDate || null, 454 | fetchOnlyUnread: fetchUnreadCheckbox ? fetchUnreadCheckbox.checked : false, 455 | ...(customPromptText && { customPromptText: customPromptText }), 456 | ...(userApiKey && { openaiApiKey: userApiKey }) 457 | }; 458 | const response = await fetch(`${BACKEND_URL}/ai/summarize`, { 459 | method: 'POST', 460 | headers: { 'Content-Type': 'application/json' }, 461 | body: JSON.stringify(requestBody) 462 | }); 463 | const data = await response.json(); 464 | if (!response.ok) throw new Error(data.error || `HTTP error ${response.status}`); 465 | addMessageToChat('assistant', `סיכום עבור ${currentSelectedChatName}:\n${data.summary}`); 466 | } catch (error) { 467 | console.error('Error summarizing group:', error); 468 | addMessageToChat('assistant', `שגיאה בסיכום: ${error.message}`, true); 469 | } finally { 470 | hideAILoading('summarize'); 471 | } 472 | }); 473 | }); 474 | 475 | askQuestionButton.addEventListener('click', async () => { 476 | const question = aiQueryInput.value.trim(); 477 | if (!question) { 478 | addMessageToChat('assistant', 'יש להקליד שאלה תחילה.', true); 479 | return; 480 | } 481 | if (!currentSelectedChatId) { 482 | addMessageToChat('assistant', 'יש לבחור קבוצה עם הודעות לפני שליחת שאלה.', true); 483 | return; 484 | } 485 | const startDate = startDateInput ? startDateInput.value.trim() : null; 486 | const endDate = endDateInput ? endDateInput.value.trim() : null; 487 | if ((startDate && !isValidDdMmYyyy(startDate)) || (endDate && !isValidDdMmYyyy(endDate))) { 488 | addMessageToChat('assistant', 'פורמט תאריך לא תקין. אנא השתמש בפורמט DD/MM/YYYY או השאר ריק.', true); 489 | return; 490 | } 491 | addMessageToChat('user', `שאלה: ${question}${startDate || endDate ? ` (בהקשר תאריכים: ${startDate || ''} - ${endDate || ''})` : ''}`); 492 | showAILoading('ask'); 493 | 494 | chrome.storage.local.get(['customSystemPrompt', 'userOpenAIApiKey'], async (storageResult) => { 495 | if (chrome.runtime.lastError) { 496 | console.error('Error getting data from storage for Q&A:', chrome.runtime.lastError); 497 | addMessageToChat('assistant', 'שגיאה בטעינת הגדרות.', true); 498 | hideAILoading('ask'); 499 | return; 500 | } 501 | const customPromptText = storageResult.customSystemPrompt || null; 502 | const userApiKey = storageResult.userOpenAIApiKey || null; 503 | 504 | let logMsg = "[PANEL.JS] For Q&A: "; 505 | logMsg += customPromptText ? "Sending custom prompt. " : "No custom prompt. "; 506 | logMsg += userApiKey ? "Sending user API key." : "No user API key."; 507 | console.log(logMsg); 508 | 509 | try { 510 | const requestBody = { 511 | chatId: currentSelectedChatId, 512 | question: question, 513 | startDate: startDate || null, 514 | endDate: endDate || null, 515 | fetchOnlyUnread: fetchUnreadCheckbox ? fetchUnreadCheckbox.checked : false, 516 | ...(customPromptText && { customPromptText: customPromptText }), 517 | ...(userApiKey && { openaiApiKey: userApiKey }) 518 | }; 519 | const response = await fetch(`${BACKEND_URL}/ai/ask`, { 520 | method: 'POST', 521 | headers: { 'Content-Type': 'application/json' }, 522 | body: JSON.stringify(requestBody) 523 | }); 524 | const data = await response.json(); 525 | if (!response.ok) throw new Error(data.error || `HTTP error ${response.status}`); 526 | addMessageToChat('assistant', `תשובה:\n${data.answer}`); 527 | aiQueryInput.value = ''; 528 | } catch (error) { 529 | console.error('Error asking question:', error); 530 | addMessageToChat('assistant', `שגיאה בקבלת תשובה: ${error.message}`, true); 531 | } finally { 532 | hideAILoading('ask'); 533 | } 534 | }); 535 | }); 536 | } 537 | 538 | async function checkBackendAndWhatsAppStatus() { 539 | const backendOk = await checkBackendConnection(); 540 | const elementsToHide = [ 541 | chatSelectorArea, summarizeButton, dateFilterAreaEl, 542 | filterOptionsLabelEl, unreadFilterAreaEl, document.querySelector('.chat-input-area') 543 | ]; 544 | 545 | if (backendOk) { 546 | try { 547 | const response = await fetch(`${BACKEND_URL}/auth/status`); 548 | if (!response.ok) { 549 | const errorData = await response.json().catch(() => ({ message: 'Failed to get WhatsApp status details.' })); 550 | throw new Error(errorData.message || `HTTP error ${response.status}`); 551 | } 552 | const status = await response.json(); 553 | updateWhatsappConnectionStatus(status.message, !status.ready && !status.qrCodeAvailable); 554 | 555 | if (status.ready) { 556 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 557 | // Filter elements visibility will be handled by initAuthAndGroupListing after groups are fetched 558 | fetchGroups(); 559 | } else if (status.qrCodeAvailable) { 560 | elementsToHide.forEach(el => { if (el) el.style.display = 'none'; }); 561 | fetchAndDisplayQrCode(); 562 | } else { 563 | elementsToHide.forEach(el => { if (el) el.style.display = 'none'; }); 564 | if(whatsappQrCodeArea) whatsappQrCodeArea.style.display = 'none'; 565 | } 566 | } catch (error) { 567 | updateWhatsappConnectionStatus(`Error checking WhatsApp: ${error.message}`, true); 568 | } 569 | } else { 570 | updateWhatsappConnectionStatus('Backend not connected. Cannot check WhatsApp status.', true); 571 | } 572 | hideInitialLoadingIndicator(); 573 | } 574 | 575 | function initAuthAndGroupListing() { 576 | if (chatSelectorDropdown) { 577 | chatSelectorDropdown.addEventListener('change', async () => { 578 | currentSelectedChatId = chatSelectorDropdown.value; 579 | const selectedOption = chatSelectorDropdown.options[chatSelectorDropdown.selectedIndex]; 580 | currentSelectedChatName = selectedOption ? selectedOption.text.split(' (')[0] : null; 581 | 582 | const chatInputArea = document.querySelector('.chat-input-area'); 583 | const elementsToShow = [summarizeButton, filterOptionsLabelEl, unreadFilterAreaEl, dateFilterAreaEl, chatInputArea]; 584 | const elementsToHide = [summarizeButton, filterOptionsLabelEl, unreadFilterAreaEl, dateFilterAreaEl, chatInputArea]; 585 | 586 | 587 | if (currentSelectedChatId) { 588 | console.log(`[PANEL.JS] Group selected: ${currentSelectedChatName} (ID: ${currentSelectedChatId})`); 589 | showStatus(`טוען הודעות עבור ${currentSelectedChatName}...`); 590 | fetchedMessagesForSelectedChat = []; 591 | 592 | elementsToShow.forEach(el => { if(el) el.style.display = el === summarizeButton || el === chatInputArea ? 'flex' : 'block';}); 593 | if(aiQueryInput) aiQueryInput.disabled = false; 594 | if(askQuestionButton) askQuestionButton.disabled = false; 595 | 596 | hideAILoading('summarize'); // Correctly sets button state 597 | hideAILoading('ask'); // Correctly sets button state 598 | 599 | try { 600 | const messageCount = 1000; 601 | const response = await fetch(`${BACKEND_URL}/groups/${currentSelectedChatId}/messages?count=${messageCount}`); 602 | if (!response.ok) { 603 | const errorData = await response.json().catch(() => ({ error: 'Failed to fetch messages.' })); 604 | throw new Error(errorData.error || `HTTP error ${response.status}`); 605 | } 606 | const messages = await response.json(); 607 | fetchedMessagesForSelectedChat = messages; 608 | console.log(`[PANEL.JS] Fetched ${fetchedMessagesForSelectedChat.length} messages for ${currentSelectedChatName}`); 609 | showStatus(`${fetchedMessagesForSelectedChat.length} הודעות נטענו עבור ${currentSelectedChatName}. מוכן לפעולות AI.`); 610 | 611 | hideAILoading('summarize'); 612 | hideAILoading('ask'); 613 | 614 | } catch (error) { 615 | console.error('[PANEL.JS] Error fetching messages:', error); 616 | showStatus(`שגיאה בטעינת הודעות: ${error.message}`, true); 617 | fetchedMessagesForSelectedChat = []; 618 | hideAILoading('summarize'); 619 | hideAILoading('ask'); 620 | } 621 | } else { 622 | currentSelectedChatName = null; 623 | fetchedMessagesForSelectedChat = []; 624 | elementsToHide.forEach(el => { if(el) el.style.display = 'none'; }); 625 | if(aiQueryInput) aiQueryInput.disabled = true; 626 | if(askQuestionButton) askQuestionButton.disabled = true; 627 | showStatus('לא נבחרה קבוצה.'); 628 | hideAILoading('summarize'); // Ensure buttons are correctly disabled/hidden 629 | hideAILoading('ask'); 630 | } 631 | }); 632 | } 633 | } 634 | 635 | let backendConnected = false; 636 | 637 | async function checkBackendConnection() { 638 | const debugInfo = document.getElementById('debug-connection-info'); 639 | try { 640 | const response = await fetch(`${BACKEND_URL}/health`); 641 | if (response.ok) { 642 | if (debugInfo) { 643 | debugInfo.textContent = `Backend: Connected`; 644 | debugInfo.style.color = 'green'; 645 | } 646 | backendConnected = true; 647 | return true; 648 | } else { 649 | if (debugInfo) { 650 | debugInfo.textContent = `Backend: Failed (Status ${response.status})`; 651 | debugInfo.style.color = 'red'; 652 | } 653 | backendConnected = false; 654 | return false; 655 | } 656 | } catch (error) { 657 | if (debugInfo) { 658 | debugInfo.textContent = `Backend: Error (${error.message.substring(0,20)}...)`; 659 | debugInfo.style.color = 'red'; 660 | } 661 | backendConnected = false; 662 | return false; 663 | } 664 | } 665 | function isBackendConnected() { return backendConnected; } 666 | 667 | function showInitialLoadingIndicator() { 668 | if (document.querySelector('.initial-loading-indicator')) return; 669 | const indicator = document.createElement('div'); 670 | indicator.className = 'initial-loading-indicator'; 671 | indicator.innerHTML = `

    Initializing WhatsApp Insights AI...

    `; 672 | document.body.appendChild(indicator); 673 | } 674 | 675 | function hideInitialLoadingIndicator() { 676 | const indicator = document.querySelector('.initial-loading-indicator'); 677 | if (indicator) indicator.remove(); 678 | } 679 | 680 | function showStatus(message, isError = false) { 681 | const statusElement = document.getElementById('ai-status-message'); 682 | if (statusElement) { 683 | statusElement.textContent = message; 684 | statusElement.className = `status-message ${isError ? 'status-error' : 'status-ok'}`; 685 | statusElement.style.display = 'block'; 686 | if (!isError) { 687 | setTimeout(() => { statusElement.style.display = 'none'; }, 4000); 688 | } 689 | } else { 690 | console.log(`Status (${isError ? 'Error' : 'Info'}): ${message}`); 691 | } 692 | } 693 | 694 | function addMessageToChat(sender, text, isError = false) { 695 | if (!aiChatMessagesDiv) { 696 | console.error("Chat message display area not found"); 697 | return; 698 | } 699 | const messageItem = document.createElement('div'); 700 | messageItem.classList.add('message', sender === 'user' ? 'user-message' : 'assistant-message'); 701 | if (isError) messageItem.classList.add('error-message'); 702 | const senderNameStrong = document.createElement('strong'); 703 | senderNameStrong.textContent = sender === 'user' ? 'You:' : 'AI Assistant:'; 704 | const messageContentDiv = document.createElement('div'); 705 | messageContentDiv.classList.add('message-content'); 706 | messageContentDiv.innerHTML = text; 707 | messageItem.appendChild(senderNameStrong); 708 | messageItem.appendChild(messageContentDiv); 709 | aiChatMessagesDiv.appendChild(messageItem); 710 | aiChatMessagesDiv.scrollTop = aiChatMessagesDiv.scrollHeight; 711 | } 712 | 713 | function setupChatInterface() { 714 | } 715 | 716 | function attachLoadingIndicators() { 717 | } 718 | 719 | function addRefreshButton() { 720 | const refreshButton = document.getElementById('refresh-button'); 721 | if (refreshButton) { 722 | refreshButton.addEventListener('click', () => { 723 | console.log("Refresh button clicked. Re-checking status."); 724 | showInitialLoadingIndicator(); 725 | checkBackendAndWhatsAppStatus(); 726 | }); 727 | } 728 | } --------------------------------------------------------------------------------