@in-the-loop-labs/pair-review 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +371 -0
- package/bin/git-diff-lines +146 -0
- package/bin/pair-review.js +49 -0
- package/package.json +71 -0
- package/public/css/ai-summary-modal.css +183 -0
- package/public/css/pr.css +8698 -0
- package/public/css/repo-settings.css +891 -0
- package/public/css/styles.css +479 -0
- package/public/favicon.png +0 -0
- package/public/index.html +1104 -0
- package/public/js/components/AIPanel.js +1639 -0
- package/public/js/components/AISummaryModal.js +278 -0
- package/public/js/components/AnalysisConfigModal.js +684 -0
- package/public/js/components/ConfirmDialog.js +227 -0
- package/public/js/components/PreviewModal.js +344 -0
- package/public/js/components/ProgressModal.js +678 -0
- package/public/js/components/ReviewModal.js +531 -0
- package/public/js/components/SplitButton.js +382 -0
- package/public/js/components/StatusIndicator.js +265 -0
- package/public/js/components/SuggestionNavigator.js +489 -0
- package/public/js/components/Toast.js +166 -0
- package/public/js/local.js +1580 -0
- package/public/js/modules/analysis-history.js +940 -0
- package/public/js/modules/comment-manager.js +643 -0
- package/public/js/modules/diff-renderer.js +585 -0
- package/public/js/modules/file-comment-manager.js +1242 -0
- package/public/js/modules/gap-coordinates.js +190 -0
- package/public/js/modules/hunk-parser.js +358 -0
- package/public/js/modules/line-tracker.js +386 -0
- package/public/js/modules/panel-resizer.js +228 -0
- package/public/js/modules/storage-cleanup.js +36 -0
- package/public/js/modules/suggestion-manager.js +692 -0
- package/public/js/pr.js +3503 -0
- package/public/js/repo-settings.js +691 -0
- package/public/js/utils/file-order.js +87 -0
- package/public/js/utils/markdown.js +97 -0
- package/public/js/utils/suggestion-ui.js +55 -0
- package/public/js/utils/tier-icons.js +25 -0
- package/public/local.html +460 -0
- package/public/pr.html +329 -0
- package/public/repo-settings.html +243 -0
- package/src/ai/analyzer.js +2592 -0
- package/src/ai/claude-cli.js +153 -0
- package/src/ai/claude-provider.js +261 -0
- package/src/ai/codex-provider.js +361 -0
- package/src/ai/copilot-provider.js +345 -0
- package/src/ai/gemini-provider.js +375 -0
- package/src/ai/index.js +47 -0
- package/src/ai/prompts/baseline/_meta.json +14 -0
- package/src/ai/prompts/baseline/level1/balanced.js +239 -0
- package/src/ai/prompts/baseline/level1/fast.js +194 -0
- package/src/ai/prompts/baseline/level1/thorough.js +319 -0
- package/src/ai/prompts/baseline/level2/balanced.js +248 -0
- package/src/ai/prompts/baseline/level2/fast.js +201 -0
- package/src/ai/prompts/baseline/level2/thorough.js +367 -0
- package/src/ai/prompts/baseline/level3/balanced.js +280 -0
- package/src/ai/prompts/baseline/level3/fast.js +220 -0
- package/src/ai/prompts/baseline/level3/thorough.js +459 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
- package/src/ai/prompts/config.js +52 -0
- package/src/ai/prompts/index.js +267 -0
- package/src/ai/prompts/shared/diff-instructions.js +50 -0
- package/src/ai/prompts/shared/output-schema.js +179 -0
- package/src/ai/prompts/shared/valid-files.js +37 -0
- package/src/ai/provider.js +260 -0
- package/src/config.js +139 -0
- package/src/database.js +2284 -0
- package/src/git/gitattributes.js +207 -0
- package/src/git/worktree.js +688 -0
- package/src/github/client.js +893 -0
- package/src/github/parser.js +247 -0
- package/src/local-review.js +691 -0
- package/src/main.js +987 -0
- package/src/routes/analysis.js +897 -0
- package/src/routes/comments.js +534 -0
- package/src/routes/config.js +250 -0
- package/src/routes/local.js +1728 -0
- package/src/routes/pr.js +1164 -0
- package/src/routes/shared.js +218 -0
- package/src/routes/worktrees.js +500 -0
- package/src/server.js +295 -0
- package/src/utils/diff-annotator.js +414 -0
- package/src/utils/instructions.js +33 -0
- package/src/utils/json-extractor.js +107 -0
- package/src/utils/line-validation.js +183 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/paths.js +161 -0
- package/src/utils/stats-calculator.js +86 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Split Button Component
|
|
4
|
+
* A button with a main action area and a dropdown menu for additional actions
|
|
5
|
+
*/
|
|
6
|
+
class SplitButton {
|
|
7
|
+
// localStorage key for persisting default action preference
|
|
8
|
+
static STORAGE_KEY = 'pair-review-split-button-default-action';
|
|
9
|
+
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.container = null;
|
|
12
|
+
this.dropdown = null;
|
|
13
|
+
this.isOpen = false;
|
|
14
|
+
this.commentCount = 0;
|
|
15
|
+
// In local mode, default to preview since submit is hidden
|
|
16
|
+
const isLocalMode = window.PAIR_REVIEW_LOCAL_MODE === true;
|
|
17
|
+
this.hideSubmit = isLocalMode || options.hideSubmit === true;
|
|
18
|
+
|
|
19
|
+
// Determine default action: local mode always uses preview,
|
|
20
|
+
// otherwise check localStorage for saved preference
|
|
21
|
+
if (isLocalMode) {
|
|
22
|
+
this.defaultAction = 'preview';
|
|
23
|
+
} else {
|
|
24
|
+
const savedAction = this.loadSavedAction();
|
|
25
|
+
this.defaultAction = savedAction || options.defaultAction || 'submit';
|
|
26
|
+
}
|
|
27
|
+
this.onSubmit = options.onSubmit || (() => {});
|
|
28
|
+
this.onPreview = options.onPreview || (() => {});
|
|
29
|
+
this.onClear = options.onClear || (() => {});
|
|
30
|
+
this.onSetDefault = options.onSetDefault || (() => {});
|
|
31
|
+
|
|
32
|
+
// Bind methods
|
|
33
|
+
this.handleMainClick = this.handleMainClick.bind(this);
|
|
34
|
+
this.handleDropdownClick = this.handleDropdownClick.bind(this);
|
|
35
|
+
this.handleOutsideClick = this.handleOutsideClick.bind(this);
|
|
36
|
+
this.handleMenuItemClick = this.handleMenuItemClick.bind(this);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create and return the split button element
|
|
41
|
+
* @returns {HTMLElement} The split button container element
|
|
42
|
+
*/
|
|
43
|
+
render() {
|
|
44
|
+
// Create container
|
|
45
|
+
this.container = document.createElement('div');
|
|
46
|
+
this.container.className = 'split-button-container';
|
|
47
|
+
this.container.id = 'comment-split-button';
|
|
48
|
+
|
|
49
|
+
// Create main button
|
|
50
|
+
const mainButton = document.createElement('button');
|
|
51
|
+
mainButton.className = 'split-button-main';
|
|
52
|
+
mainButton.id = 'split-button-main';
|
|
53
|
+
mainButton.type = 'button';
|
|
54
|
+
mainButton.addEventListener('click', this.handleMainClick);
|
|
55
|
+
|
|
56
|
+
// Create button text span
|
|
57
|
+
const buttonText = document.createElement('span');
|
|
58
|
+
buttonText.className = 'split-button-text';
|
|
59
|
+
buttonText.id = 'split-button-text';
|
|
60
|
+
buttonText.textContent = this.getButtonText();
|
|
61
|
+
mainButton.appendChild(buttonText);
|
|
62
|
+
|
|
63
|
+
// Create dropdown toggle button
|
|
64
|
+
const dropdownToggle = document.createElement('button');
|
|
65
|
+
dropdownToggle.className = 'split-button-dropdown-toggle';
|
|
66
|
+
dropdownToggle.id = 'split-button-dropdown-toggle';
|
|
67
|
+
dropdownToggle.type = 'button';
|
|
68
|
+
dropdownToggle.setAttribute('aria-label', 'Open comment actions menu');
|
|
69
|
+
dropdownToggle.setAttribute('aria-haspopup', 'true');
|
|
70
|
+
dropdownToggle.setAttribute('aria-expanded', 'false');
|
|
71
|
+
dropdownToggle.addEventListener('click', this.handleDropdownClick);
|
|
72
|
+
|
|
73
|
+
// Add dropdown arrow icon
|
|
74
|
+
dropdownToggle.innerHTML = `
|
|
75
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
76
|
+
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
|
77
|
+
</svg>
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
// Create dropdown menu
|
|
81
|
+
this.dropdown = document.createElement('div');
|
|
82
|
+
this.dropdown.className = 'split-button-dropdown';
|
|
83
|
+
this.dropdown.id = 'split-button-dropdown';
|
|
84
|
+
this.dropdown.setAttribute('role', 'menu');
|
|
85
|
+
this.dropdown.style.display = 'none';
|
|
86
|
+
|
|
87
|
+
this.updateDropdownMenu();
|
|
88
|
+
|
|
89
|
+
// Assemble the split button
|
|
90
|
+
this.container.appendChild(mainButton);
|
|
91
|
+
this.container.appendChild(dropdownToggle);
|
|
92
|
+
this.container.appendChild(this.dropdown);
|
|
93
|
+
|
|
94
|
+
return this.container;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update the dropdown menu items based on current state
|
|
99
|
+
*/
|
|
100
|
+
updateDropdownMenu() {
|
|
101
|
+
if (!this.dropdown) return;
|
|
102
|
+
|
|
103
|
+
const isSubmitDefault = this.defaultAction === 'submit';
|
|
104
|
+
const isPreviewDefault = this.defaultAction === 'preview';
|
|
105
|
+
|
|
106
|
+
// Build menu items - conditionally include Submit Review
|
|
107
|
+
let menuItems = '';
|
|
108
|
+
|
|
109
|
+
if (!this.hideSubmit) {
|
|
110
|
+
menuItems += `
|
|
111
|
+
<button class="split-button-menu-item" data-action="submit" role="menuitem">
|
|
112
|
+
<span class="menu-item-check">${isSubmitDefault ? '✓' : ''}</span>
|
|
113
|
+
<span class="menu-item-text">Submit Review</span>
|
|
114
|
+
</button>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
menuItems += `
|
|
118
|
+
<button class="split-button-menu-item" data-action="preview" role="menuitem">
|
|
119
|
+
<span class="menu-item-check">${isPreviewDefault ? '✓' : ''}</span>
|
|
120
|
+
<span class="menu-item-text">Preview</span>
|
|
121
|
+
</button>
|
|
122
|
+
<div class="split-button-menu-separator"></div>
|
|
123
|
+
<button class="split-button-menu-item split-button-menu-item-danger" data-action="clear" role="menuitem" ${this.commentCount === 0 ? 'disabled' : ''}>
|
|
124
|
+
<span class="menu-item-check"></span>
|
|
125
|
+
<span class="menu-item-text">Clear All</span>
|
|
126
|
+
</button>
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
this.dropdown.innerHTML = menuItems;
|
|
130
|
+
|
|
131
|
+
// Add click handlers to menu items
|
|
132
|
+
this.dropdown.querySelectorAll('.split-button-menu-item').forEach(item => {
|
|
133
|
+
item.addEventListener('click', this.handleMenuItemClick);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle click on the main button area
|
|
139
|
+
*/
|
|
140
|
+
handleMainClick() {
|
|
141
|
+
if (this.defaultAction === 'submit') {
|
|
142
|
+
this.onSubmit();
|
|
143
|
+
} else {
|
|
144
|
+
this.onPreview();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle click on the dropdown toggle
|
|
150
|
+
* @param {Event} event - Click event
|
|
151
|
+
*/
|
|
152
|
+
handleDropdownClick(event) {
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
this.toggleDropdown();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle click on a menu item
|
|
159
|
+
* @param {Event} event - Click event
|
|
160
|
+
*/
|
|
161
|
+
handleMenuItemClick(event) {
|
|
162
|
+
const button = event.currentTarget;
|
|
163
|
+
if (button.disabled) return;
|
|
164
|
+
|
|
165
|
+
const action = button.dataset.action;
|
|
166
|
+
|
|
167
|
+
switch (action) {
|
|
168
|
+
case 'submit':
|
|
169
|
+
if (this.defaultAction !== 'submit') {
|
|
170
|
+
this.setDefaultAction('submit');
|
|
171
|
+
}
|
|
172
|
+
this.onSubmit();
|
|
173
|
+
break;
|
|
174
|
+
case 'preview':
|
|
175
|
+
if (this.defaultAction !== 'preview') {
|
|
176
|
+
this.setDefaultAction('preview');
|
|
177
|
+
}
|
|
178
|
+
this.onPreview();
|
|
179
|
+
break;
|
|
180
|
+
case 'clear':
|
|
181
|
+
this.onClear();
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.closeDropdown();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Toggle dropdown open/close state
|
|
190
|
+
*/
|
|
191
|
+
toggleDropdown() {
|
|
192
|
+
if (this.isOpen) {
|
|
193
|
+
this.closeDropdown();
|
|
194
|
+
} else {
|
|
195
|
+
this.openDropdown();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Open the dropdown menu
|
|
201
|
+
*/
|
|
202
|
+
openDropdown() {
|
|
203
|
+
if (!this.dropdown) return;
|
|
204
|
+
|
|
205
|
+
this.isOpen = true;
|
|
206
|
+
this.dropdown.style.display = 'block';
|
|
207
|
+
|
|
208
|
+
const toggleButton = this.container?.querySelector('#split-button-dropdown-toggle');
|
|
209
|
+
if (toggleButton) {
|
|
210
|
+
toggleButton.setAttribute('aria-expanded', 'true');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.container?.classList.add('split-button-open');
|
|
214
|
+
|
|
215
|
+
// Add outside click listener
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
document.addEventListener('click', this.handleOutsideClick);
|
|
218
|
+
}, 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Close the dropdown menu
|
|
223
|
+
*/
|
|
224
|
+
closeDropdown() {
|
|
225
|
+
if (!this.dropdown) return;
|
|
226
|
+
|
|
227
|
+
this.isOpen = false;
|
|
228
|
+
this.dropdown.style.display = 'none';
|
|
229
|
+
|
|
230
|
+
const toggleButton = this.container?.querySelector('#split-button-dropdown-toggle');
|
|
231
|
+
if (toggleButton) {
|
|
232
|
+
toggleButton.setAttribute('aria-expanded', 'false');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.container?.classList.remove('split-button-open');
|
|
236
|
+
|
|
237
|
+
// Remove outside click listener
|
|
238
|
+
document.removeEventListener('click', this.handleOutsideClick);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handle clicks outside the dropdown to close it
|
|
243
|
+
* @param {Event} event - Click event
|
|
244
|
+
*/
|
|
245
|
+
handleOutsideClick(event) {
|
|
246
|
+
if (this.container && !this.container.contains(event.target)) {
|
|
247
|
+
this.closeDropdown();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Set the default action for the main button
|
|
253
|
+
* @param {string} action - 'submit' or 'preview'
|
|
254
|
+
*/
|
|
255
|
+
setDefaultAction(action) {
|
|
256
|
+
if (action !== 'submit' && action !== 'preview') return;
|
|
257
|
+
|
|
258
|
+
// In local mode (hideSubmit), don't allow setting submit as default
|
|
259
|
+
if (this.hideSubmit && action === 'submit') {
|
|
260
|
+
action = 'preview';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.defaultAction = action;
|
|
264
|
+
this.updateDropdownMenu();
|
|
265
|
+
this.updateButtonText();
|
|
266
|
+
this.onSetDefault(action);
|
|
267
|
+
|
|
268
|
+
// Persist to localStorage (only in PR mode where submit is available)
|
|
269
|
+
if (!this.hideSubmit) {
|
|
270
|
+
this.saveAction(action);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Load saved action preference from localStorage
|
|
276
|
+
* @returns {string|null} Saved action or null if not found
|
|
277
|
+
*/
|
|
278
|
+
loadSavedAction() {
|
|
279
|
+
try {
|
|
280
|
+
const saved = localStorage.getItem(SplitButton.STORAGE_KEY);
|
|
281
|
+
if (saved === 'submit' || saved === 'preview') {
|
|
282
|
+
return saved;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// localStorage may be unavailable (private browsing, etc.)
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Save action preference to localStorage
|
|
292
|
+
* @param {string} action - 'submit' or 'preview'
|
|
293
|
+
*/
|
|
294
|
+
saveAction(action) {
|
|
295
|
+
try {
|
|
296
|
+
localStorage.setItem(SplitButton.STORAGE_KEY, action);
|
|
297
|
+
} catch {
|
|
298
|
+
// localStorage may be unavailable (private browsing, etc.)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the button text based on default action and comment count
|
|
304
|
+
* @returns {string} Button text
|
|
305
|
+
*/
|
|
306
|
+
getButtonText() {
|
|
307
|
+
const actionText = this.defaultAction === 'submit' ? 'Submit Review' : 'Preview';
|
|
308
|
+
if (this.commentCount > 0) {
|
|
309
|
+
return `${actionText} (${this.commentCount})`;
|
|
310
|
+
}
|
|
311
|
+
return actionText;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Update the button text display
|
|
316
|
+
*/
|
|
317
|
+
updateButtonText() {
|
|
318
|
+
const textSpan = this.container?.querySelector('#split-button-text');
|
|
319
|
+
if (textSpan) {
|
|
320
|
+
textSpan.textContent = this.getButtonText();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Update the comment count display
|
|
326
|
+
* @param {number} count - Number of comments
|
|
327
|
+
*/
|
|
328
|
+
updateCommentCount(count) {
|
|
329
|
+
this.commentCount = count;
|
|
330
|
+
this.updateButtonText();
|
|
331
|
+
|
|
332
|
+
// Update button styling based on count
|
|
333
|
+
const mainButton = this.container?.querySelector('#split-button-main');
|
|
334
|
+
const dropdownToggle = this.container?.querySelector('#split-button-dropdown-toggle');
|
|
335
|
+
|
|
336
|
+
if (mainButton) {
|
|
337
|
+
if (count > 0) {
|
|
338
|
+
mainButton.classList.add('has-comments');
|
|
339
|
+
} else {
|
|
340
|
+
mainButton.classList.remove('has-comments');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (dropdownToggle) {
|
|
345
|
+
if (count > 0) {
|
|
346
|
+
dropdownToggle.classList.add('has-comments');
|
|
347
|
+
} else {
|
|
348
|
+
dropdownToggle.classList.remove('has-comments');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Update the Clear All menu item disabled state
|
|
353
|
+
this.updateDropdownMenu();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the current comment count
|
|
358
|
+
* @returns {number} Current comment count
|
|
359
|
+
*/
|
|
360
|
+
getCommentCount() {
|
|
361
|
+
return this.commentCount;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Destroy the component and clean up event listeners
|
|
366
|
+
*/
|
|
367
|
+
destroy() {
|
|
368
|
+
document.removeEventListener('click', this.handleOutsideClick);
|
|
369
|
+
|
|
370
|
+
if (this.container) {
|
|
371
|
+
this.container.remove();
|
|
372
|
+
this.container = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.dropdown = null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Export for use in other modules
|
|
380
|
+
if (typeof window !== 'undefined') {
|
|
381
|
+
window.SplitButton = SplitButton;
|
|
382
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Status Indicator Component
|
|
4
|
+
* Shows AI analysis status in the toolbar when running in background
|
|
5
|
+
*/
|
|
6
|
+
class StatusIndicator {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.indicator = null;
|
|
9
|
+
this.currentAnalysisId = null;
|
|
10
|
+
this.animationDots = 0;
|
|
11
|
+
this.dotsInterval = null;
|
|
12
|
+
|
|
13
|
+
this.createIndicator();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the status indicator DOM structure
|
|
18
|
+
*/
|
|
19
|
+
createIndicator() {
|
|
20
|
+
// Remove existing indicator if it exists
|
|
21
|
+
const existing = document.getElementById('status-indicator');
|
|
22
|
+
if (existing) {
|
|
23
|
+
existing.remove();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create status indicator element
|
|
27
|
+
const indicator = document.createElement('div');
|
|
28
|
+
indicator.id = 'status-indicator';
|
|
29
|
+
indicator.className = 'status-indicator';
|
|
30
|
+
indicator.style.display = 'none';
|
|
31
|
+
|
|
32
|
+
indicator.innerHTML = `
|
|
33
|
+
<div class="status-content" onclick="statusIndicator.reopenModal()">
|
|
34
|
+
<span class="status-icon">
|
|
35
|
+
<svg class="spinner" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
36
|
+
<path d="M8 0a8 8 0 0 0-8 8h2a6 6 0 0 1 6-6V0z"/>
|
|
37
|
+
</svg>
|
|
38
|
+
</span>
|
|
39
|
+
<span class="status-text">AI analyzing</span>
|
|
40
|
+
<span class="status-dots">...</span>
|
|
41
|
+
</div>
|
|
42
|
+
<button class="status-close" onclick="statusIndicator.hide()" title="Dismiss">
|
|
43
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
44
|
+
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
|
|
45
|
+
</svg>
|
|
46
|
+
</button>
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
// Insert indicator into the PR actions area
|
|
50
|
+
const prActions = document.querySelector('.pr-actions');
|
|
51
|
+
if (prActions) {
|
|
52
|
+
// Insert before the first button (theme toggle)
|
|
53
|
+
const firstButton = prActions.querySelector('button');
|
|
54
|
+
if (firstButton) {
|
|
55
|
+
prActions.insertBefore(indicator, firstButton);
|
|
56
|
+
} else {
|
|
57
|
+
prActions.appendChild(indicator);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Fallback: append to body
|
|
61
|
+
document.body.appendChild(indicator);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.indicator = indicator;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Show the status indicator
|
|
69
|
+
* @param {string} analysisId - Analysis ID being tracked
|
|
70
|
+
* @param {string} text - Initial status text
|
|
71
|
+
*/
|
|
72
|
+
show(analysisId, text = 'AI analyzing') {
|
|
73
|
+
if (!this.indicator) {
|
|
74
|
+
this.createIndicator();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.currentAnalysisId = analysisId;
|
|
78
|
+
this.indicator.style.display = 'flex';
|
|
79
|
+
|
|
80
|
+
// Set initial state
|
|
81
|
+
this.updateText(text);
|
|
82
|
+
this.showSpinner();
|
|
83
|
+
this.startDotsAnimation();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hide the status indicator
|
|
88
|
+
*/
|
|
89
|
+
hide() {
|
|
90
|
+
if (this.indicator) {
|
|
91
|
+
this.indicator.style.display = 'none';
|
|
92
|
+
}
|
|
93
|
+
this.stopDotsAnimation();
|
|
94
|
+
this.currentAnalysisId = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update status text
|
|
99
|
+
* @param {string} text - New status text
|
|
100
|
+
*/
|
|
101
|
+
updateText(text) {
|
|
102
|
+
const statusText = this.indicator?.querySelector('.status-text');
|
|
103
|
+
if (statusText) {
|
|
104
|
+
statusText.textContent = text;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show completion state
|
|
110
|
+
* @param {string} message - Completion message
|
|
111
|
+
*/
|
|
112
|
+
showComplete(message = 'Analysis complete') {
|
|
113
|
+
this.updateText(message);
|
|
114
|
+
this.showCheckmark();
|
|
115
|
+
this.stopDotsAnimation();
|
|
116
|
+
|
|
117
|
+
// CRITICAL FIX: Automatically reload AI suggestions when background analysis completes
|
|
118
|
+
console.log('Background analysis completed, reloading AI suggestions...');
|
|
119
|
+
if (window.prManager && typeof window.prManager.loadAISuggestions === 'function') {
|
|
120
|
+
window.prManager.loadAISuggestions()
|
|
121
|
+
.then(() => {
|
|
122
|
+
console.log('AI suggestions reloaded successfully after background analysis');
|
|
123
|
+
// Auto-hide after suggestions have loaded successfully (shorter delay)
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
this.hide();
|
|
126
|
+
}, 3000);
|
|
127
|
+
})
|
|
128
|
+
.catch(error => {
|
|
129
|
+
console.error('Error reloading AI suggestions after background analysis:', error);
|
|
130
|
+
// Auto-hide with longer delay if loading failed
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
this.hide();
|
|
133
|
+
}, 7000);
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
console.warn('PRManager not available for automatic suggestion reload after background analysis');
|
|
137
|
+
// Auto-hide after 5 seconds if no PR manager available
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
this.hide();
|
|
140
|
+
}, 5000);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Show error state
|
|
146
|
+
* @param {string} message - Error message
|
|
147
|
+
*/
|
|
148
|
+
showError(message = 'Analysis failed') {
|
|
149
|
+
this.updateText(message);
|
|
150
|
+
this.showErrorIcon();
|
|
151
|
+
this.stopDotsAnimation();
|
|
152
|
+
|
|
153
|
+
// Auto-hide after 10 seconds
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
this.hide();
|
|
156
|
+
}, 10000);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Show spinner icon
|
|
161
|
+
*/
|
|
162
|
+
showSpinner() {
|
|
163
|
+
const statusIcon = this.indicator?.querySelector('.status-icon');
|
|
164
|
+
if (statusIcon) {
|
|
165
|
+
statusIcon.innerHTML = `
|
|
166
|
+
<svg class="spinner" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
167
|
+
<path d="M8 0a8 8 0 0 0-8 8h2a6 6 0 0 1 6-6V0z"/>
|
|
168
|
+
</svg>
|
|
169
|
+
`;
|
|
170
|
+
statusIcon.className = 'status-icon spinning';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Show checkmark icon
|
|
176
|
+
*/
|
|
177
|
+
showCheckmark() {
|
|
178
|
+
const statusIcon = this.indicator?.querySelector('.status-icon');
|
|
179
|
+
if (statusIcon) {
|
|
180
|
+
statusIcon.innerHTML = `
|
|
181
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" class="checkmark">
|
|
182
|
+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
183
|
+
</svg>
|
|
184
|
+
`;
|
|
185
|
+
statusIcon.className = 'status-icon success';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Show error icon
|
|
191
|
+
*/
|
|
192
|
+
showErrorIcon() {
|
|
193
|
+
const statusIcon = this.indicator?.querySelector('.status-icon');
|
|
194
|
+
if (statusIcon) {
|
|
195
|
+
statusIcon.innerHTML = `
|
|
196
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" class="error-icon">
|
|
197
|
+
<path d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
|
|
198
|
+
</svg>
|
|
199
|
+
`;
|
|
200
|
+
statusIcon.className = 'status-icon error';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Start animated dots
|
|
206
|
+
*/
|
|
207
|
+
startDotsAnimation() {
|
|
208
|
+
this.stopDotsAnimation(); // Clear any existing interval
|
|
209
|
+
|
|
210
|
+
this.dotsInterval = setInterval(() => {
|
|
211
|
+
const dotsElement = this.indicator?.querySelector('.status-dots');
|
|
212
|
+
if (!dotsElement) return;
|
|
213
|
+
|
|
214
|
+
this.animationDots = (this.animationDots + 1) % 4;
|
|
215
|
+
dotsElement.textContent = '.'.repeat(this.animationDots);
|
|
216
|
+
}, 500);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Stop animated dots
|
|
221
|
+
*/
|
|
222
|
+
stopDotsAnimation() {
|
|
223
|
+
if (this.dotsInterval) {
|
|
224
|
+
clearInterval(this.dotsInterval);
|
|
225
|
+
this.dotsInterval = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const dotsElement = this.indicator?.querySelector('.status-dots');
|
|
229
|
+
if (dotsElement) {
|
|
230
|
+
dotsElement.textContent = '';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Reopen modal from status indicator
|
|
236
|
+
*/
|
|
237
|
+
reopenModal() {
|
|
238
|
+
if (this.currentAnalysisId && window.progressModal) {
|
|
239
|
+
window.progressModal.reopenFromBackground();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Ensure indicator is properly positioned in header
|
|
245
|
+
*/
|
|
246
|
+
repositionInHeader() {
|
|
247
|
+
// This method can be called after the header is re-rendered
|
|
248
|
+
// to ensure the indicator is in the correct position
|
|
249
|
+
const prActions = document.querySelector('.pr-actions');
|
|
250
|
+
const existingIndicator = document.getElementById('status-indicator');
|
|
251
|
+
|
|
252
|
+
if (prActions && existingIndicator && !prActions.contains(existingIndicator)) {
|
|
253
|
+
// Move the indicator to the correct position
|
|
254
|
+
const firstButton = prActions.querySelector('button');
|
|
255
|
+
if (firstButton) {
|
|
256
|
+
prActions.insertBefore(existingIndicator, firstButton);
|
|
257
|
+
} else {
|
|
258
|
+
prActions.appendChild(existingIndicator);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Initialize global instance
|
|
265
|
+
window.statusIndicator = new StatusIndicator();
|