Skip to content

Commit ca4cbb6

Browse files
committed
refactor: decompose 4 largest JS files into 14 focused modules
- Split ai-assistant.js (2026→1033) → ai-chat.js, ai-actions.js, ai-image.js - Split ai-docgen.js (1314→347) → ai-docgen-generate.js, ai-docgen-ui.js - Split executable-blocks.js (1136→114) → exec-math.js, exec-python.js, exec-sandbox.js - Split table-tools.js (1017→475) → table-sort-filter.js, table-analytics.js - Internal namespaces (M._ai, M._docgen, M._exec, M._table) for cross-module state - Phased dynamic imports in main.js (Phase 2 → Phase 2.5 for dependent sub-modules) - Build passes, all 12 Playwright smoke tests pass
1 parent f9850a9 commit ca4cbb6

17 files changed

+3718
-3678
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ TextAgent has undergone significant evolution since its inception. What started
373373

374374
| Date | Feature / Update |
375375
|------|-----------------|
376+
| **2026-03-09** | 🧩 **File Decomposition** — split 4 largest JS modules (~5,500 lines) into 14 focused files: `ai-assistant.js` → 4 modules (core, chat, actions, image); `ai-docgen.js` → 3 modules (core, generate, ui); `executable-blocks.js` → 4 modules (core bash, math, python, sandbox); `table-tools.js` → 3 modules (core, sort-filter, analytics); internal namespaces (`M._ai`, `M._docgen`, `M._exec`, `M._table`) for cross-module communication; phased dynamic imports in `main.js` |
376377
| **2026-03-09** |**Bundle Size Reduction** — lazy-loaded mermaid (~518 KB), deferred Phase 2–5 feature modules (AI, exporters, speech, templates, docgen) via `requestIdleCallback`, removed `manualChunks` Vite config; startup bundle reduced from ~4.6 MB to ~1.6 MB (65% reduction); converters, export, math, and mermaid chunks now load on demand |
377378
| **2026-03-09** | 🛠️ **Quality & Config Alignment** — fixed copy-button selector mismatch (`copy-md-button``copy-markdown-button`); unified preview-theme storage key to `md-viewer-preview-theme`; HTML export now self-contained with all CSS inlined + theme attributes; PDF export reuses shared rendering pipeline (`renderMarkdownToContainer`); aligned license to MIT across `package.json`, `LICENSE`, and `README`; unified changelog path to `changelogs/` in pre-commit hook + GitHub Actions; removed duplicate `public/firestore.rules` and `public/nginx.conf`; repaired desktop `prepare.js` (removed stale `script.js` copy) and updated `desktop-app/README.md`; added ESLint, Prettier, and Playwright with 4 smoke tests (import, export, share, view-mode) |
378379
| **2026-03-08** | 🐧 **Compile & Run**`{{Linux:}}` tag now supports `Language:` + `Script:` fields for compiling and executing 25+ languages (C, C++, Rust, Go, Java, Python, TypeScript, Kotlin, Scala, Ruby, Swift, Haskell, Dart, C#, PHP, Lua…) via [Judge0 CE](https://ce.judge0.com); inline output with stdout, stderr, compile errors, execution time & memory stats; 10 new language-specific coding templates |
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# File Decomposition — Refactor
2+
3+
- Split `ai-assistant.js` (2026→1033 lines) into 4 modules: core, `ai-chat.js`, `ai-actions.js`, `ai-image.js`
4+
- Split `ai-docgen.js` (1314→347 lines) into 3 modules: core, `ai-docgen-generate.js`, `ai-docgen-ui.js`
5+
- Split `executable-blocks.js` (1136→114 lines) into 4 modules: core bash, `exec-math.js`, `exec-python.js`, `exec-sandbox.js`
6+
- Split `table-tools.js` (1017→475 lines) into 3 modules: core, `table-sort-filter.js`, `table-analytics.js`
7+
- Added internal namespaces (`M._ai`, `M._docgen`, `M._exec`, `M._table`) for cross-module state sharing
8+
- Updated `src/main.js` with phased dynamic imports (Phase 2 → Phase 2.5 for dependent sub-modules)
9+
10+
---
11+
12+
## Summary
13+
Split 4 large JavaScript files (~5,500 total lines) into 14 focused modules to improve maintainability and isolate regressions by feature/service. Each module is encapsulated in an IIFE and uses internal namespaces for cross-module communication.
14+
15+
---
16+
17+
## 1. AI Assistant Decomposition
18+
**Files:** `js/ai-assistant.js`, `js/ai-chat.js`, `js/ai-actions.js`, `js/ai-image.js`
19+
**What:** Extracted chat UI/messaging/streaming to `ai-chat.js`, quick action chips/context menu/autocomplete to `ai-actions.js`, and image generation to `ai-image.js`. Core retains AI panel logic, model management, and worker lifecycle. Shared state via `M._ai`.
20+
**Impact:** Regression in chat, actions, or image gen no longer risks breaking core AI panel initialization.
21+
22+
## 2. AI DocGen Decomposition
23+
**Files:** `js/ai-docgen.js`, `js/ai-docgen-generate.js`, `js/ai-docgen-ui.js`
24+
**What:** Extracted generation/review/agent flow logic to `ai-docgen-generate.js` and fill-all/toast/progress UI to `ai-docgen-ui.js`. Core retains tag parsing and preview rendering. Shared state via `M._docgen`.
25+
**Impact:** Generation prompts and UI components can be modified independently of tag parsing.
26+
27+
## 3. Executable Blocks Decomposition
28+
**Files:** `js/executable-blocks.js`, `js/exec-math.js`, `js/exec-python.js`, `js/exec-sandbox.js`
29+
**What:** Extracted Math.js/LaTeX/Nerdamer to `exec-math.js`, Python/Pyodide to `exec-python.js`, and HTML/JS/SQL sandboxes to `exec-sandbox.js`. Core retains Bash execution + shared `escapeHtml` via `M._exec`.
30+
**Impact:** Each language runtime is now independently maintainable; adding new languages requires only a new file.
31+
32+
## 4. Table Tools Decomposition
33+
**Files:** `js/table-tools.js`, `js/table-sort-filter.js`, `js/table-analytics.js`
34+
**What:** Extracted sort/filter/search to `table-sort-filter.js` and stats/chart to `table-analytics.js`. Core retains toolbar setup, cell editing, row/col CRUD, and export. Late-binding via `M._table`.
35+
**Impact:** Sort/filter and analytics features can be modified without touching core table rendering.
36+
37+
## 5. Import Loading Phases
38+
**Files:** `src/main.js`
39+
**What:** Added Phase 2.5 for sub-modules that depend on Phase 2 namespaces (`M._exec`, `M._table`). All 10 new modules loaded in parallel within their respective phases.
40+
**Impact:** Correct dependency ordering ensures namespace objects exist before sub-modules access them.
41+
42+
---
43+
44+
## Files Changed (15 total)
45+
46+
| File | Lines Changed | Type |
47+
|------|:---:|------|
48+
| `js/ai-assistant.js` | −993 | Trimmed core (extracted chat, actions, image) |
49+
| `js/ai-chat.js` | +430 | NEW — Chat UI, messaging, streaming |
50+
| `js/ai-actions.js` | +190 | NEW — Quick actions, context menu, autocomplete |
51+
| `js/ai-image.js` | +220 | NEW — Image generation |
52+
| `js/ai-docgen.js` | −967 | Trimmed core (extracted generation, UI) |
53+
| `js/ai-docgen-generate.js` | +470 | NEW — Generation, review, agent flow |
54+
| `js/ai-docgen-ui.js` | +200 | NEW — Fill-all, toast, download prompts |
55+
| `js/executable-blocks.js` | −1022 | Trimmed core (extracted math, python, sandbox) |
56+
| `js/exec-math.js` | +300 | NEW — Math.js + LaTeX/Nerdamer |
57+
| `js/exec-python.js` | +170 | NEW — Python/Pyodide WASM |
58+
| `js/exec-sandbox.js` | +320 | NEW — HTML, JS, SQL sandboxes |
59+
| `js/table-tools.js` | −542 | Trimmed core (extracted sort/filter, analytics) |
60+
| `js/table-sort-filter.js` | +255 | NEW — Sort, filter, search |
61+
| `js/table-analytics.js` | +296 | NEW — Stats + bar chart |
62+
| `src/main.js` | +9 | Added Phase 2.5 imports |

js/ai-actions.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// ============================================
2+
// ai-actions.js — Quick Action Chips, Context Menu, Ctrl+Space
3+
// Extracted from ai-assistant.js for modularity
4+
// ============================================
5+
(function (M) {
6+
'use strict';
7+
8+
var _ai = M._ai;
9+
var markdownEditor = M.markdownEditor;
10+
var previewPane = M.previewPane;
11+
12+
// --- DOM elements ---
13+
var aiInput = document.getElementById('ai-input');
14+
var aiContextMenu = document.getElementById('ai-context-menu');
15+
var aiPanel = document.getElementById('ai-panel');
16+
var aiPanelOverlay = document.getElementById('ai-panel-overlay');
17+
var aiToggleBtn = document.getElementById('ai-toggle-button');
18+
19+
// --- Quick Action Chips ---
20+
document.querySelectorAll('.ai-action-chip').forEach(function (chip) {
21+
chip.addEventListener('click', function () {
22+
var action = this.dataset.action;
23+
var savedSelection = _ai.savedSelection || { start: 0, end: 0 };
24+
// Check editor selection first, then preview selection
25+
var selectedText = markdownEditor.value.substring(savedSelection.start, savedSelection.end);
26+
if (!selectedText) {
27+
var sel = window.getSelection();
28+
selectedText = sel ? sel.toString().trim() : '';
29+
}
30+
var editorContent = markdownEditor.value;
31+
32+
var isReady = _ai.isCurrentModelReady();
33+
if (!isReady) {
34+
M.openAiPanel();
35+
return;
36+
}
37+
38+
// Ensure panel is open
39+
if (!_ai.panelOpen) M.openAiPanel();
40+
41+
switch (action) {
42+
case 'summarize':
43+
case 'expand':
44+
case 'rephrase':
45+
case 'grammar':
46+
case 'polish':
47+
case 'formalize':
48+
case 'elaborate':
49+
case 'shorten': {
50+
if (!editorContent.trim() && !selectedText.trim()) {
51+
_ai.addAiMessage('Please add some text in the editor first.');
52+
return;
53+
}
54+
if (selectedText) {
55+
_ai.addAiMessage('Using selected text (' + selectedText.length + ' chars)', 'user');
56+
_ai.sendToAi(action, selectedText, null);
57+
} else if (editorContent.length > 1500) {
58+
_ai.processDocumentInChunks(action, editorContent);
59+
} else {
60+
_ai.addAiMessage('Using entire document (' + editorContent.length + ' chars)', 'user');
61+
_ai.sendToAi(action, editorContent, null);
62+
}
63+
break;
64+
}
65+
case 'explain':
66+
case 'simplify':
67+
if (!selectedText) {
68+
_ai.addAiMessage('Please select some text in the editor to explain or simplify.');
69+
return;
70+
}
71+
_ai.sendToAi(action, selectedText, 'Please ' + action + ' this text.');
72+
break;
73+
case 'autocomplete': {
74+
var textBeforeCursor = editorContent.substring(0, savedSelection.start);
75+
if (!textBeforeCursor.trim()) {
76+
_ai.addAiMessage('Place your cursor after some text in the editor to auto-complete.');
77+
return;
78+
}
79+
_ai.sendToAi('autocomplete', textBeforeCursor, null);
80+
break;
81+
}
82+
case 'markdown':
83+
aiInput.placeholder = 'Describe what markdown to generate...';
84+
aiInput.focus();
85+
break;
86+
}
87+
});
88+
});
89+
90+
// --- Ctrl+Space for Auto-Complete ---
91+
markdownEditor.addEventListener('keydown', function (e) {
92+
if ((e.ctrlKey || e.metaKey) && e.key === ' ') {
93+
e.preventDefault();
94+
var isReady = _ai.isCurrentModelReady();
95+
if (!isReady) {
96+
M.openAiPanel();
97+
return;
98+
}
99+
if (!_ai.panelOpen) M.openAiPanel();
100+
101+
var textBeforeCursor = markdownEditor.value.substring(0, markdownEditor.selectionStart);
102+
if (textBeforeCursor.trim()) {
103+
_ai.sendToAi('autocomplete', textBeforeCursor, null);
104+
}
105+
}
106+
});
107+
108+
// --- Context Menu (on text selection in editor OR preview) ---
109+
var contextMenuTimeout = null;
110+
var savedContextText = '';
111+
112+
// Editor text selection
113+
markdownEditor.addEventListener('mouseup', function (e) {
114+
clearTimeout(contextMenuTimeout);
115+
contextMenuTimeout = setTimeout(function () {
116+
var selectedText = markdownEditor.value.substring(
117+
markdownEditor.selectionStart,
118+
markdownEditor.selectionEnd
119+
);
120+
121+
var isReady = _ai.isCurrentModelReady();
122+
if (selectedText && selectedText.length > 2 && isReady) {
123+
savedContextText = selectedText;
124+
aiContextMenu.style.left = Math.min(e.clientX, window.innerWidth - 180) + 'px';
125+
aiContextMenu.style.top = Math.min(e.clientY - 10, window.innerHeight - 250) + 'px';
126+
aiContextMenu.style.display = 'flex';
127+
} else {
128+
aiContextMenu.style.display = 'none';
129+
}
130+
}, 300);
131+
});
132+
133+
// Preview pane text selection
134+
if (previewPane) {
135+
previewPane.addEventListener('mouseup', function (e) {
136+
clearTimeout(contextMenuTimeout);
137+
contextMenuTimeout = setTimeout(function () {
138+
var selection = window.getSelection();
139+
var selectedText = selection ? selection.toString().trim() : '';
140+
141+
var isReady = _ai.isCurrentModelReady();
142+
if (selectedText && selectedText.length > 2 && isReady) {
143+
savedContextText = selectedText;
144+
aiContextMenu.style.left = Math.min(e.clientX, window.innerWidth - 180) + 'px';
145+
aiContextMenu.style.top = Math.min(e.clientY - 10, window.innerHeight - 250) + 'px';
146+
aiContextMenu.style.display = 'flex';
147+
} else {
148+
aiContextMenu.style.display = 'none';
149+
}
150+
}, 300);
151+
});
152+
}
153+
154+
// Hide context menu on click elsewhere
155+
document.addEventListener('mousedown', function (e) {
156+
if (aiContextMenu && aiContextMenu.style.display !== 'none' && !aiContextMenu.contains(e.target)) {
157+
aiContextMenu.style.display = 'none';
158+
}
159+
});
160+
161+
// Context menu actions
162+
function handleContextAction(action) {
163+
clearTimeout(contextMenuTimeout);
164+
aiContextMenu.style.display = 'none';
165+
166+
if (!savedContextText) return;
167+
168+
// Open panel if needed
169+
if (!_ai.panelOpen) {
170+
aiPanel.style.display = 'flex';
171+
aiPanelOverlay.classList.add('active');
172+
void aiPanel.offsetWidth;
173+
aiPanel.classList.add('ai-panel-open');
174+
aiToggleBtn.classList.add('ai-active');
175+
_ai.panelOpen = true;
176+
document.body.classList.add('ai-panel-active');
177+
}
178+
179+
if (['summarize', 'expand', 'rephrase', 'grammar', 'explain', 'simplify', 'polish', 'formalize', 'elaborate', 'shorten'].includes(action)) {
180+
_ai.sendToAi(action, savedContextText, null);
181+
} else {
182+
_ai.sendToAi(action, savedContextText, 'Please ' + action + ' this text.');
183+
}
184+
}
185+
186+
var ctxBtns = aiContextMenu.querySelectorAll('.ai-ctx-btn');
187+
ctxBtns.forEach(function (btn) {
188+
btn.addEventListener('mousedown', function (e) {
189+
e.preventDefault();
190+
e.stopPropagation();
191+
var action = this.dataset.action;
192+
if (action) {
193+
setTimeout(function () { handleContextAction(action); }, 0);
194+
}
195+
});
196+
});
197+
198+
})(window.MDView);

0 commit comments

Comments
 (0)