git-reviewer 2.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 +21 -0
- package/README.md +282 -0
- package/bin/reviewer.js +146 -0
- package/lib/agent.js +182 -0
- package/lib/browser.js +67 -0
- package/lib/changes.js +84 -0
- package/lib/cli.js +234 -0
- package/lib/comments.js +98 -0
- package/lib/diff.js +92 -0
- package/lib/paths.js +52 -0
- package/lib/review.js +206 -0
- package/lib/sessions.js +62 -0
- package/lib/store.js +113 -0
- package/package.json +48 -0
- package/public/app.js +1439 -0
- package/public/favicon.svg +7 -0
- package/public/index.html +66 -0
- package/public/style.css +990 -0
- package/server.js +421 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
// Use current origin for Electron compatibility (dynamic port)
|
|
2
|
+
// In web mode, this will be http://localhost:4500
|
|
3
|
+
// In Electron, this will be the random port Electron assigned
|
|
4
|
+
const API_BASE = `${window.location.origin}/api`;
|
|
5
|
+
|
|
6
|
+
let currentRepoId = null;
|
|
7
|
+
let currentFiles = [];
|
|
8
|
+
let currentFile = null;
|
|
9
|
+
let currentDiffLines = [];
|
|
10
|
+
let comments = [];
|
|
11
|
+
|
|
12
|
+
// Auto-save comments to backend
|
|
13
|
+
async function saveCommentsToBackend() {
|
|
14
|
+
if (!currentRepoId) return;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await fetch(`${API_BASE}/save-comments`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
repoId: currentRepoId,
|
|
22
|
+
comments: comments // Send empty array if no comments
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Failed to save comments:', error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Load comments from backend
|
|
31
|
+
async function loadCommentsFromBackend() {
|
|
32
|
+
if (!currentRepoId) return [];
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${API_BASE}/load-comments/${currentRepoId}`);
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
return data.comments || [];
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Failed to load comments:', error);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fuzzy match saved comments to current diff
|
|
45
|
+
function matchCommentsToDiff(savedComments, filePath, diffLines) {
|
|
46
|
+
const fileComments = savedComments.filter(c => c.file === filePath);
|
|
47
|
+
const matched = [];
|
|
48
|
+
|
|
49
|
+
fileComments.forEach(savedComment => {
|
|
50
|
+
let matchResult = null;
|
|
51
|
+
|
|
52
|
+
// Try exact match first (line number + content)
|
|
53
|
+
const exactMatch = diffLines.findIndex(dl =>
|
|
54
|
+
(dl.newLine === savedComment.line || dl.oldLine === savedComment.line) &&
|
|
55
|
+
dl.content === savedComment.lineContent
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (exactMatch !== -1) {
|
|
59
|
+
matchResult = {
|
|
60
|
+
...savedComment,
|
|
61
|
+
diffLineIndex: exactMatch,
|
|
62
|
+
matchType: 'exact'
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// Try fuzzy match (search ±5 lines for similar content)
|
|
66
|
+
let bestMatch = -1;
|
|
67
|
+
let bestSimilarity = 0;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < diffLines.length; i++) {
|
|
70
|
+
const dl = diffLines[i];
|
|
71
|
+
const lineNum = dl.newLine || dl.oldLine;
|
|
72
|
+
|
|
73
|
+
// Check if within ±5 lines
|
|
74
|
+
if (Math.abs(lineNum - savedComment.line) <= 5) {
|
|
75
|
+
const similarity = calculateSimilarity(dl.content, savedComment.lineContent);
|
|
76
|
+
if (similarity > bestSimilarity && similarity > 0.6) {
|
|
77
|
+
bestSimilarity = similarity;
|
|
78
|
+
bestMatch = i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (bestMatch !== -1) {
|
|
84
|
+
matchResult = {
|
|
85
|
+
...savedComment,
|
|
86
|
+
diffLineIndex: bestMatch,
|
|
87
|
+
matchType: 'fuzzy',
|
|
88
|
+
newLineContent: diffLines[bestMatch].content
|
|
89
|
+
};
|
|
90
|
+
} else {
|
|
91
|
+
// No match found - mark as unmatched
|
|
92
|
+
matchResult = {
|
|
93
|
+
...savedComment,
|
|
94
|
+
diffLineIndex: null,
|
|
95
|
+
matchType: 'unmatched'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (matchResult) {
|
|
101
|
+
matched.push(matchResult);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return matched;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Simple similarity calculation (Levenshtein-based approximation)
|
|
109
|
+
function calculateSimilarity(str1, str2) {
|
|
110
|
+
if (str1 === str2) return 1;
|
|
111
|
+
if (!str1 || !str2) return 0;
|
|
112
|
+
|
|
113
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
114
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
115
|
+
|
|
116
|
+
if (longer.length === 0) return 1;
|
|
117
|
+
|
|
118
|
+
// Simple matching: count common characters
|
|
119
|
+
const longerLower = longer.toLowerCase().trim();
|
|
120
|
+
const shorterLower = shorter.toLowerCase().trim();
|
|
121
|
+
|
|
122
|
+
if (longerLower.includes(shorterLower) || shorterLower.includes(longerLower)) {
|
|
123
|
+
return 0.8;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Count matching words
|
|
127
|
+
const words1 = str1.toLowerCase().split(/\s+/);
|
|
128
|
+
const words2 = str2.toLowerCase().split(/\s+/);
|
|
129
|
+
const commonWords = words1.filter(w => words2.includes(w));
|
|
130
|
+
|
|
131
|
+
return commonWords.length / Math.max(words1.length, words2.length);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Load local repository
|
|
135
|
+
async function loadRepo() {
|
|
136
|
+
const repoPath = document.getElementById('repoPath').value.trim();
|
|
137
|
+
|
|
138
|
+
if (!repoPath) {
|
|
139
|
+
showStatus('Please enter a repository path', 'error');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
showStatus('Loading repository...', 'loading');
|
|
144
|
+
document.getElementById('loadBtn').disabled = true;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(`${API_BASE}/load-repo`, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ repoPath })
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(data.error || 'Failed to load repository');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
currentRepoId = data.repoId;
|
|
160
|
+
currentFiles = data.files;
|
|
161
|
+
|
|
162
|
+
if (data.files.length === 0) {
|
|
163
|
+
showStatus('No uncommitted changes found in the repository', 'error');
|
|
164
|
+
document.getElementById('loadBtn').disabled = false;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
showStatus(data.message, 'success');
|
|
169
|
+
displayFiles(data.files);
|
|
170
|
+
|
|
171
|
+
// Load saved comments
|
|
172
|
+
const savedComments = await loadCommentsFromBackend();
|
|
173
|
+
if (savedComments.length > 0) {
|
|
174
|
+
// Store saved comments in a global variable for matching later
|
|
175
|
+
window.savedComments = savedComments;
|
|
176
|
+
|
|
177
|
+
// Add all saved comments to the comments array immediately
|
|
178
|
+
comments = savedComments.map(c => ({
|
|
179
|
+
...c,
|
|
180
|
+
diffLineIndex: null, // Will be updated when file is loaded
|
|
181
|
+
matchType: c.matchType || 'exact'
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
// Show comments sidebar immediately
|
|
185
|
+
updateCommentsSidebar();
|
|
186
|
+
|
|
187
|
+
showStatus(`${data.message} - Loaded ${savedComments.length} saved comment(s)`, 'success');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
} catch (error) {
|
|
191
|
+
showStatus(`Error: ${error.message}`, 'error');
|
|
192
|
+
} finally {
|
|
193
|
+
document.getElementById('loadBtn').disabled = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Display list of changed files
|
|
198
|
+
function displayFiles(files) {
|
|
199
|
+
const sidebar = document.getElementById('sidebar');
|
|
200
|
+
const filesList = document.getElementById('filesList');
|
|
201
|
+
const fileCount = document.getElementById('fileCount');
|
|
202
|
+
const submitBtn = document.getElementById('submitReviewBtn');
|
|
203
|
+
const resizeHandle = document.getElementById('resizeHandle');
|
|
204
|
+
|
|
205
|
+
filesList.innerHTML = files.map((fileObj, index) => {
|
|
206
|
+
const file = fileObj.path;
|
|
207
|
+
const status = fileObj.status;
|
|
208
|
+
const parts = file.split('/');
|
|
209
|
+
const filename = parts[parts.length - 1];
|
|
210
|
+
const path = parts.slice(0, -1).join('/');
|
|
211
|
+
|
|
212
|
+
// Because of RTL, we need to reverse the order in HTML
|
|
213
|
+
const displayText = path ? `${path}/<span class="filename">${filename}</span>` : `<span class="filename">${filename}</span>`;
|
|
214
|
+
const statusClass = status === 'A' ? 'file-status-added' : 'file-status-modified';
|
|
215
|
+
const statusBadge = `<span class="file-status ${statusClass}">${status}</span>`;
|
|
216
|
+
|
|
217
|
+
return `<div class="file-item" onclick="loadFile('${file}', ${index})" title="${escapeHtml(file)}">
|
|
218
|
+
${statusBadge}<span class="file-path-text">${displayText}</span>
|
|
219
|
+
</div>`;
|
|
220
|
+
}).join('');
|
|
221
|
+
|
|
222
|
+
fileCount.textContent = `${files.length} file${files.length !== 1 ? 's' : ''}`;
|
|
223
|
+
sidebar.classList.remove('hidden');
|
|
224
|
+
resizeHandle.classList.remove('hidden');
|
|
225
|
+
submitBtn.classList.remove('hidden');
|
|
226
|
+
|
|
227
|
+
// Update comments sidebar
|
|
228
|
+
updateCommentsSidebar();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Load file content
|
|
232
|
+
async function loadFile(filePath, index) {
|
|
233
|
+
if (!currentRepoId) return Promise.resolve();
|
|
234
|
+
|
|
235
|
+
currentFile = filePath;
|
|
236
|
+
currentFileIndex = index; // Track for keyboard navigation
|
|
237
|
+
|
|
238
|
+
// Reset full context mode when switching files
|
|
239
|
+
isFullContextMode = false;
|
|
240
|
+
storedFullFileLines = null;
|
|
241
|
+
|
|
242
|
+
// Update active file highlight and scroll into view
|
|
243
|
+
document.querySelectorAll('.file-item').forEach((el, i) => {
|
|
244
|
+
el.classList.toggle('active', i === index);
|
|
245
|
+
if (i === index) {
|
|
246
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(`${API_BASE}/file/${currentRepoId}/${filePath}`);
|
|
252
|
+
const data = await response.json();
|
|
253
|
+
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error(data.error || 'Failed to load file');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Match saved comments to current diff if available
|
|
259
|
+
if (window.savedComments && window.savedComments.length > 0) {
|
|
260
|
+
const matchedComments = matchCommentsToDiff(window.savedComments, filePath, data.diffLines);
|
|
261
|
+
|
|
262
|
+
// Update existing comments in the array with matched diff indices
|
|
263
|
+
matchedComments.forEach(mc => {
|
|
264
|
+
const existing = comments.find(c =>
|
|
265
|
+
c.file === mc.file &&
|
|
266
|
+
c.line === mc.line &&
|
|
267
|
+
c.text === mc.text
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (existing) {
|
|
271
|
+
// Update existing comment with new diff line index and match type
|
|
272
|
+
existing.diffLineIndex = mc.diffLineIndex;
|
|
273
|
+
existing.matchType = mc.matchType;
|
|
274
|
+
existing.newLineContent = mc.newLineContent;
|
|
275
|
+
// Preserve followUps from saved data
|
|
276
|
+
if (mc.followUps) {
|
|
277
|
+
existing.followUps = mc.followUps;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
// Add new comment if it doesn't exist
|
|
281
|
+
comments.push({
|
|
282
|
+
file: mc.file,
|
|
283
|
+
diffLineIndex: mc.diffLineIndex,
|
|
284
|
+
line: mc.line,
|
|
285
|
+
lineContent: mc.lineContent,
|
|
286
|
+
selectedText: mc.selectedText,
|
|
287
|
+
text: mc.text,
|
|
288
|
+
matchType: mc.matchType,
|
|
289
|
+
newLineContent: mc.newLineContent,
|
|
290
|
+
followUps: mc.followUps || []
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Update comments sidebar after matching comments
|
|
296
|
+
updateCommentsSidebar();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
displayCode(data.filePath, data.diffLines);
|
|
300
|
+
updateFullContextButton();
|
|
301
|
+
return Promise.resolve();
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
showStatus(`Error loading file: ${error.message}`, 'error');
|
|
305
|
+
return Promise.reject(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Display code with diff highlighting
|
|
310
|
+
function displayCode(filePath, diffLines) {
|
|
311
|
+
const codeSection = document.getElementById('codeSection');
|
|
312
|
+
const currentFileEl = document.getElementById('currentFile');
|
|
313
|
+
const codeViewer = document.getElementById('codeViewer');
|
|
314
|
+
|
|
315
|
+
currentFileEl.textContent = filePath;
|
|
316
|
+
currentDiffLines = diffLines; // Store for reference when adding comments
|
|
317
|
+
|
|
318
|
+
const fileComments = comments.filter(c => c.file === filePath);
|
|
319
|
+
|
|
320
|
+
let html = '';
|
|
321
|
+
diffLines.forEach((diffLine, index) => {
|
|
322
|
+
// Use diffLineIndex to uniquely identify each line in the diff
|
|
323
|
+
const diffLineIndex = index;
|
|
324
|
+
const hasComment = fileComments.find(c => c.diffLineIndex === diffLineIndex);
|
|
325
|
+
|
|
326
|
+
const lineClass = `line diff-${diffLine.type} ${hasComment ? 'commented' : ''}`;
|
|
327
|
+
|
|
328
|
+
// Use the appropriate line number for clicking (newLine for add/unchanged, oldLine for delete)
|
|
329
|
+
const clickableLineNum = diffLine.newLine || diffLine.oldLine;
|
|
330
|
+
|
|
331
|
+
// Make both old and new line numbers clickable
|
|
332
|
+
const oldLineClick = diffLine.type === 'delete' ? `onclick="toggleCommentInput(${diffLineIndex})"` : '';
|
|
333
|
+
const newLineClick = diffLine.type !== 'delete' ? `onclick="toggleCommentInput(${diffLineIndex})"` : '';
|
|
334
|
+
|
|
335
|
+
html += `
|
|
336
|
+
<div class="${lineClass}" data-diff-index="${diffLineIndex}">
|
|
337
|
+
${hasComment ? '<span class="comment-indicator"></span>' : ''}
|
|
338
|
+
<div class="line-numbers">
|
|
339
|
+
<span class="old-line-number" ${oldLineClick}>${diffLine.oldLine || ''}</span>
|
|
340
|
+
<span class="new-line-number" ${newLineClick}>${diffLine.newLine || ''}</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="line-content">${escapeHtml(diffLine.content) || ' '}</div>
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
if (hasComment) {
|
|
347
|
+
const selectedTextAttr = hasComment.selectedText ? `'${escapeHtml(hasComment.selectedText).replace(/'/g, "\\'")}'` : 'null';
|
|
348
|
+
const matchTypeClass = hasComment.matchType === 'fuzzy' ? 'comment-box-fuzzy' : '';
|
|
349
|
+
const fuzzyWarning = hasComment.matchType === 'fuzzy' ? `
|
|
350
|
+
<div class="comment-fuzzy-warning">
|
|
351
|
+
⚠️ Code changed - comment anchored to similar line
|
|
352
|
+
${hasComment.newLineContent !== hasComment.lineContent ? `<div class="comment-original-code">Original: <code>${escapeHtml(hasComment.lineContent)}</code></div>` : ''}
|
|
353
|
+
</div>
|
|
354
|
+
` : '';
|
|
355
|
+
|
|
356
|
+
// Generate follow-ups HTML
|
|
357
|
+
let followUpsHtml = '';
|
|
358
|
+
if (hasComment.followUps && hasComment.followUps.length > 0) {
|
|
359
|
+
followUpsHtml = '<div class="comment-followups">';
|
|
360
|
+
hasComment.followUps.forEach((followUp, idx) => {
|
|
361
|
+
const timestamp = followUp.timestamp ? new Date(followUp.timestamp).toLocaleString() : '';
|
|
362
|
+
followUpsHtml += `
|
|
363
|
+
<div class="comment-followup-item">
|
|
364
|
+
<span class="followup-text">${escapeHtml(followUp.text)}</span>
|
|
365
|
+
${timestamp ? `<span class="followup-timestamp">${timestamp}</span>` : ''}
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
});
|
|
369
|
+
followUpsHtml += '</div>';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
html += `
|
|
373
|
+
<div class="comment-box ${matchTypeClass}">
|
|
374
|
+
${fuzzyWarning}
|
|
375
|
+
<span class="comment-text">${escapeHtml(hasComment.text)}</span>
|
|
376
|
+
${followUpsHtml}
|
|
377
|
+
<div class="comment-actions">
|
|
378
|
+
<button class="comment-reply" onclick="addFollowUp(${diffLineIndex})">Reply</button>
|
|
379
|
+
<button class="comment-edit" onclick="editComment(${diffLineIndex}, ${selectedTextAttr})">Edit</button>
|
|
380
|
+
<button class="comment-delete" onclick="deleteComment(${diffLineIndex})">Delete</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Show unmatched comments at the end
|
|
388
|
+
const unmatchedComments = fileComments.filter(c => c.matchType === 'unmatched');
|
|
389
|
+
if (unmatchedComments.length > 0) {
|
|
390
|
+
html += '<div class="unmatched-comments-section">';
|
|
391
|
+
unmatchedComments.forEach(comment => {
|
|
392
|
+
const commentId = `unmatched-${Math.random().toString(36).substr(2, 9)}`;
|
|
393
|
+
|
|
394
|
+
// Generate follow-ups for unmatched comments
|
|
395
|
+
let unmatchedFollowUpsHtml = '';
|
|
396
|
+
if (comment.followUps && comment.followUps.length > 0) {
|
|
397
|
+
unmatchedFollowUpsHtml = '<div class="comment-followups">';
|
|
398
|
+
comment.followUps.forEach((followUp, idx) => {
|
|
399
|
+
const timestamp = followUp.timestamp ? new Date(followUp.timestamp).toLocaleString() : '';
|
|
400
|
+
unmatchedFollowUpsHtml += `
|
|
401
|
+
<div class="comment-followup-item">
|
|
402
|
+
<span class="followup-text">${escapeHtml(followUp.text)}</span>
|
|
403
|
+
${timestamp ? `<span class="followup-timestamp">${timestamp}</span>` : ''}
|
|
404
|
+
</div>
|
|
405
|
+
`;
|
|
406
|
+
});
|
|
407
|
+
unmatchedFollowUpsHtml += '</div>';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
html += `
|
|
411
|
+
<div class="unmatched-comment-item collapsed" id="${commentId}">
|
|
412
|
+
<div class="unmatched-comment-header" onclick="toggleUnmatchedComment('${commentId}')">
|
|
413
|
+
<span class="unmatched-comment-icon">⚠️</span>
|
|
414
|
+
<span class="unmatched-comment-title">Comment not found (Line ${comment.line} changed)</span>
|
|
415
|
+
<span class="unmatched-comment-toggle">▶</span>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="unmatched-comment-body">
|
|
418
|
+
<div class="unmatched-comment-original">
|
|
419
|
+
<strong>Original line:</strong>
|
|
420
|
+
<code>${escapeHtml(comment.lineContent)}</code>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="comment-text">${escapeHtml(comment.text)}</div>
|
|
423
|
+
${comment.selectedText ? `<div class="comment-item-selected">${escapeHtml(comment.selectedText)}</div>` : ''}
|
|
424
|
+
${unmatchedFollowUpsHtml}
|
|
425
|
+
<div class="comment-actions">
|
|
426
|
+
<button class="comment-reply" onclick="addFollowUpToUnmatched('${comment.file.replace(/'/g, "\\'")}', ${comment.line}, '${comment.text.replace(/'/g, "\\'")}')">Reply</button>
|
|
427
|
+
<button class="comment-delete" onclick="deleteUnmatchedComment('${comment.file.replace(/'/g, "\\'")}', ${comment.line}, '${comment.text.replace(/'/g, "\\'")}')">Delete</button>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
`;
|
|
432
|
+
});
|
|
433
|
+
html += '</div>';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
codeViewer.innerHTML = html;
|
|
437
|
+
codeSection.classList.remove('hidden');
|
|
438
|
+
|
|
439
|
+
// Add text selection handler
|
|
440
|
+
codeViewer.removeEventListener('mousedown', trackShiftKeyDown);
|
|
441
|
+
codeViewer.removeEventListener('mouseup', handleTextSelection);
|
|
442
|
+
codeViewer.addEventListener('mousedown', trackShiftKeyDown);
|
|
443
|
+
codeViewer.addEventListener('mouseup', handleTextSelection);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Toggle unmatched comment expansion
|
|
447
|
+
function toggleUnmatchedComment(commentId) {
|
|
448
|
+
const element = document.getElementById(commentId);
|
|
449
|
+
if (!element) return;
|
|
450
|
+
|
|
451
|
+
element.classList.toggle('collapsed');
|
|
452
|
+
const toggle = element.querySelector('.unmatched-comment-toggle');
|
|
453
|
+
if (toggle) {
|
|
454
|
+
toggle.textContent = element.classList.contains('collapsed') ? '▶' : '▼';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Delete unmatched comment
|
|
459
|
+
function deleteUnmatchedComment(file, line, text) {
|
|
460
|
+
comments = comments.filter(c => !(c.file === file && c.line === line && c.text === text));
|
|
461
|
+
|
|
462
|
+
// Auto-save to backend
|
|
463
|
+
saveCommentsToBackend();
|
|
464
|
+
|
|
465
|
+
// Update comments sidebar
|
|
466
|
+
updateCommentsSidebar();
|
|
467
|
+
|
|
468
|
+
// Reload the current file to remove the comment
|
|
469
|
+
if (currentFile === file) {
|
|
470
|
+
const fileIndex = currentFiles.findIndex(f => f.path === file);
|
|
471
|
+
loadFile(file, fileIndex);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Add follow-up to a regular comment
|
|
476
|
+
function addFollowUp(diffLineIndex) {
|
|
477
|
+
// Remove any existing follow-up input
|
|
478
|
+
const existingInput = document.querySelector('.followup-input-box');
|
|
479
|
+
if (existingInput) {
|
|
480
|
+
existingInput.remove();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Find the comment box
|
|
484
|
+
const lineEl = document.querySelector(`.line[data-diff-index="${diffLineIndex}"]`);
|
|
485
|
+
if (!lineEl) return;
|
|
486
|
+
|
|
487
|
+
const commentBox = lineEl.nextElementSibling;
|
|
488
|
+
if (!commentBox || !commentBox.classList.contains('comment-box')) return;
|
|
489
|
+
|
|
490
|
+
// Create follow-up input
|
|
491
|
+
const inputBox = document.createElement('div');
|
|
492
|
+
inputBox.className = 'followup-input-box';
|
|
493
|
+
inputBox.innerHTML = `
|
|
494
|
+
<textarea placeholder="Enter your follow-up (Cmd/Ctrl+Enter to save)..." id="followupInput"></textarea>
|
|
495
|
+
<div class="actions">
|
|
496
|
+
<button onclick="saveFollowUp(${diffLineIndex})">Add Follow-up</button>
|
|
497
|
+
<button class="cancel-btn" onclick="this.closest('.followup-input-box').remove()">Cancel</button>
|
|
498
|
+
</div>
|
|
499
|
+
`;
|
|
500
|
+
|
|
501
|
+
// Insert after the comment actions
|
|
502
|
+
const actionsDiv = commentBox.querySelector('.comment-actions');
|
|
503
|
+
if (actionsDiv) {
|
|
504
|
+
actionsDiv.after(inputBox);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const textarea = document.getElementById('followupInput');
|
|
508
|
+
textarea.focus();
|
|
509
|
+
|
|
510
|
+
// Auto-resize textarea
|
|
511
|
+
const autoResize = () => {
|
|
512
|
+
textarea.style.height = 'auto';
|
|
513
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
textarea.addEventListener('input', autoResize);
|
|
517
|
+
autoResize();
|
|
518
|
+
|
|
519
|
+
// Add keyboard shortcuts
|
|
520
|
+
textarea.addEventListener('keydown', (e) => {
|
|
521
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
saveFollowUp(diffLineIndex);
|
|
524
|
+
}
|
|
525
|
+
if (e.key === 'Escape') {
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
inputBox.remove();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Save follow-up to a regular comment
|
|
533
|
+
function saveFollowUp(diffLineIndex) {
|
|
534
|
+
const input = document.getElementById('followupInput');
|
|
535
|
+
const text = input?.value.trim();
|
|
536
|
+
|
|
537
|
+
if (!text) {
|
|
538
|
+
showStatus('Please enter a follow-up comment', 'error');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const comment = comments.find(c => c.file === currentFile && c.diffLineIndex === diffLineIndex);
|
|
543
|
+
if (!comment) return;
|
|
544
|
+
|
|
545
|
+
// Initialize followUps if it doesn't exist
|
|
546
|
+
if (!comment.followUps) {
|
|
547
|
+
comment.followUps = [];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
comment.followUps.push({
|
|
551
|
+
text: text,
|
|
552
|
+
timestamp: new Date().toISOString()
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Auto-save to backend
|
|
556
|
+
saveCommentsToBackend();
|
|
557
|
+
|
|
558
|
+
// Update comments sidebar
|
|
559
|
+
updateCommentsSidebar();
|
|
560
|
+
|
|
561
|
+
// Reload the current file to show the follow-up
|
|
562
|
+
const fileIndex = currentFiles.findIndex(f => f.path === currentFile);
|
|
563
|
+
loadFile(currentFile, fileIndex);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Add follow-up to an unmatched comment
|
|
567
|
+
function addFollowUpToUnmatched(file, line, text) {
|
|
568
|
+
// Remove any existing follow-up input
|
|
569
|
+
const existingInput = document.querySelector('.followup-input-box');
|
|
570
|
+
if (existingInput) {
|
|
571
|
+
existingInput.remove();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Find the unmatched comment element
|
|
575
|
+
const unmatchedItems = document.querySelectorAll('.unmatched-comment-item');
|
|
576
|
+
let targetItem = null;
|
|
577
|
+
|
|
578
|
+
unmatchedItems.forEach(item => {
|
|
579
|
+
const itemText = item.querySelector('.comment-text')?.textContent;
|
|
580
|
+
const itemLine = item.querySelector('.unmatched-comment-title')?.textContent;
|
|
581
|
+
if (itemText === text && itemLine?.includes(`Line ${line}`)) {
|
|
582
|
+
targetItem = item;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!targetItem) return;
|
|
587
|
+
|
|
588
|
+
const commentBody = targetItem.querySelector('.unmatched-comment-body');
|
|
589
|
+
if (!commentBody) return;
|
|
590
|
+
|
|
591
|
+
// Create follow-up input
|
|
592
|
+
const inputBox = document.createElement('div');
|
|
593
|
+
inputBox.className = 'followup-input-box';
|
|
594
|
+
|
|
595
|
+
// Generate unique ID for this specific input
|
|
596
|
+
const inputId = `followupInput-${Math.random().toString(36).substr(2, 9)}`;
|
|
597
|
+
|
|
598
|
+
inputBox.innerHTML = `
|
|
599
|
+
<textarea placeholder="Enter your follow-up (Cmd/Ctrl+Enter to save)..." id="${inputId}"></textarea>
|
|
600
|
+
<div class="actions">
|
|
601
|
+
<button onclick="saveFollowUpToUnmatched('${file.replace(/'/g, "\\'")}', ${line}, '${text.replace(/'/g, "\\'")}', '${inputId}')">Add Follow-up</button>
|
|
602
|
+
<button class="cancel-btn" onclick="this.closest('.followup-input-box').remove()">Cancel</button>
|
|
603
|
+
</div>
|
|
604
|
+
`;
|
|
605
|
+
|
|
606
|
+
// Insert before the comment actions
|
|
607
|
+
const actionsDiv = commentBody.querySelector('.comment-actions');
|
|
608
|
+
if (actionsDiv) {
|
|
609
|
+
actionsDiv.before(inputBox);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const textarea = document.getElementById(inputId);
|
|
613
|
+
textarea.focus();
|
|
614
|
+
|
|
615
|
+
// Auto-resize textarea
|
|
616
|
+
const autoResize = () => {
|
|
617
|
+
textarea.style.height = 'auto';
|
|
618
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
textarea.addEventListener('input', autoResize);
|
|
622
|
+
autoResize();
|
|
623
|
+
|
|
624
|
+
// Add keyboard shortcuts
|
|
625
|
+
textarea.addEventListener('keydown', (e) => {
|
|
626
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
saveFollowUpToUnmatched(file, line, text, inputId);
|
|
629
|
+
}
|
|
630
|
+
if (e.key === 'Escape') {
|
|
631
|
+
e.preventDefault();
|
|
632
|
+
inputBox.remove();
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Save follow-up to an unmatched comment
|
|
638
|
+
function saveFollowUpToUnmatched(file, line, text, inputId) {
|
|
639
|
+
const input = document.getElementById(inputId);
|
|
640
|
+
const followUpText = input?.value.trim();
|
|
641
|
+
|
|
642
|
+
if (!followUpText) {
|
|
643
|
+
showStatus('Please enter a follow-up comment', 'error');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const comment = comments.find(c => c.file === file && c.line === line && c.text === text);
|
|
648
|
+
if (!comment) return;
|
|
649
|
+
|
|
650
|
+
// Initialize followUps if it doesn't exist
|
|
651
|
+
if (!comment.followUps) {
|
|
652
|
+
comment.followUps = [];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
comment.followUps.push({
|
|
656
|
+
text: followUpText,
|
|
657
|
+
timestamp: new Date().toISOString()
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Auto-save to backend
|
|
661
|
+
saveCommentsToBackend();
|
|
662
|
+
|
|
663
|
+
// Update comments sidebar
|
|
664
|
+
updateCommentsSidebar();
|
|
665
|
+
|
|
666
|
+
// Reload the current file to show the follow-up
|
|
667
|
+
if (currentFile === file) {
|
|
668
|
+
const fileIndex = currentFiles.findIndex(f => f.path === file);
|
|
669
|
+
loadFile(file, fileIndex);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Track command key state during selection
|
|
674
|
+
let commandKeyPressed = false;
|
|
675
|
+
|
|
676
|
+
// Track command key on mousedown
|
|
677
|
+
function trackShiftKeyDown(e) {
|
|
678
|
+
commandKeyPressed = e.metaKey;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Handle text selection in code viewer
|
|
682
|
+
function handleTextSelection(e) {
|
|
683
|
+
const selection = window.getSelection();
|
|
684
|
+
const rawSelectedText = selection.toString().trim();
|
|
685
|
+
|
|
686
|
+
if (!rawSelectedText) {
|
|
687
|
+
commandKeyPressed = false;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Only trigger comment input if Command key was held during selection
|
|
692
|
+
if (!commandKeyPressed) {
|
|
693
|
+
commandKeyPressed = false;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Reset command key tracking
|
|
698
|
+
commandKeyPressed = false;
|
|
699
|
+
|
|
700
|
+
// Extract only the code content, excluding line numbers
|
|
701
|
+
const range = selection.getRangeAt(0);
|
|
702
|
+
const container = document.createElement('div');
|
|
703
|
+
container.appendChild(range.cloneContents());
|
|
704
|
+
|
|
705
|
+
// Remove all line numbers from the cloned selection
|
|
706
|
+
const lineNumbers = container.querySelectorAll('.line-numbers, .old-line-number, .new-line-number');
|
|
707
|
+
lineNumbers.forEach(el => el.remove());
|
|
708
|
+
|
|
709
|
+
// Extract text from each .line-content separately to avoid extra whitespace
|
|
710
|
+
const lineContents = container.querySelectorAll('.line-content');
|
|
711
|
+
let selectedText = '';
|
|
712
|
+
|
|
713
|
+
if (lineContents.length > 0) {
|
|
714
|
+
// Get text from each line-content and join with single newlines
|
|
715
|
+
const lines = Array.from(lineContents).map(el => el.textContent);
|
|
716
|
+
selectedText = lines.join('\n').trim();
|
|
717
|
+
} else {
|
|
718
|
+
// Fallback to full textContent if no line-content elements found
|
|
719
|
+
selectedText = container.textContent.trim();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!selectedText) {
|
|
723
|
+
selection.removeAllRanges();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Find the line element where selection ends
|
|
728
|
+
let targetElement = selection.focusNode;
|
|
729
|
+
|
|
730
|
+
// Traverse up to find the line element
|
|
731
|
+
while (targetElement && !targetElement.classList?.contains('line')) {
|
|
732
|
+
targetElement = targetElement.parentElement;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!targetElement) {
|
|
736
|
+
selection.removeAllRanges();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const diffLineIndex = parseInt(targetElement.dataset.diffIndex);
|
|
741
|
+
if (diffLineIndex === undefined || isNaN(diffLineIndex)) {
|
|
742
|
+
selection.removeAllRanges();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Clear selection
|
|
747
|
+
selection.removeAllRanges();
|
|
748
|
+
|
|
749
|
+
// Show comment input with selected text
|
|
750
|
+
toggleCommentInputWithSelection(diffLineIndex, selectedText);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Toggle comment input
|
|
754
|
+
function toggleCommentInput(diffLineIndex) {
|
|
755
|
+
toggleCommentInputWithSelection(diffLineIndex, null);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Toggle comment input with optional selected text
|
|
759
|
+
function toggleCommentInputWithSelection(diffLineIndex, selectedText = null) {
|
|
760
|
+
const existing = document.querySelector('.comment-input-box');
|
|
761
|
+
if (existing) {
|
|
762
|
+
existing.remove();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check if comment already exists
|
|
766
|
+
if (comments.find(c => c.file === currentFile && c.diffLineIndex === diffLineIndex)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const lineEl = document.querySelector(`.line[data-diff-index="${diffLineIndex}"]`);
|
|
771
|
+
const inputBox = document.createElement('div');
|
|
772
|
+
inputBox.className = 'comment-input-box';
|
|
773
|
+
|
|
774
|
+
// Show selected text if available
|
|
775
|
+
const selectedTextHtml = selectedText
|
|
776
|
+
? `<div class="selected-text-preview">
|
|
777
|
+
<strong>Selected code:</strong>
|
|
778
|
+
<pre>${escapeHtml(selectedText)}</pre>
|
|
779
|
+
</div>`
|
|
780
|
+
: '';
|
|
781
|
+
|
|
782
|
+
inputBox.innerHTML = `
|
|
783
|
+
${selectedTextHtml}
|
|
784
|
+
<textarea placeholder="Enter your comment (Cmd/Ctrl+Enter to save)..." id="commentInput"></textarea>
|
|
785
|
+
<div class="actions">
|
|
786
|
+
<button onclick="saveComment(${diffLineIndex}, ${selectedText ? `\`${escapeHtml(selectedText).replace(/`/g, '\\`')}\`` : 'null'})">Save Comment</button>
|
|
787
|
+
<button class="cancel-btn" onclick="this.closest('.comment-input-box').remove()">Cancel</button>
|
|
788
|
+
</div>
|
|
789
|
+
`;
|
|
790
|
+
|
|
791
|
+
lineEl.after(inputBox);
|
|
792
|
+
const textarea = document.getElementById('commentInput');
|
|
793
|
+
textarea.focus();
|
|
794
|
+
|
|
795
|
+
// Auto-resize textarea
|
|
796
|
+
const autoResize = () => {
|
|
797
|
+
textarea.style.height = 'auto';
|
|
798
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
textarea.addEventListener('input', autoResize);
|
|
802
|
+
autoResize(); // Initial resize
|
|
803
|
+
|
|
804
|
+
// Add keyboard shortcuts
|
|
805
|
+
textarea.addEventListener('keydown', (e) => {
|
|
806
|
+
// Cmd/Ctrl+Enter to save
|
|
807
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
saveComment(diffLineIndex, selectedText);
|
|
810
|
+
}
|
|
811
|
+
// Escape to close
|
|
812
|
+
if (e.key === 'Escape') {
|
|
813
|
+
e.preventDefault();
|
|
814
|
+
inputBox.remove();
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Save comment
|
|
820
|
+
function saveComment(diffLineIndex, selectedText = null) {
|
|
821
|
+
const input = document.getElementById('commentInput');
|
|
822
|
+
const text = input.value.trim();
|
|
823
|
+
|
|
824
|
+
if (!text) {
|
|
825
|
+
showStatus('Please enter a comment', 'error');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Find the diff line
|
|
830
|
+
const diffLine = currentDiffLines[diffLineIndex];
|
|
831
|
+
if (!diffLine) return;
|
|
832
|
+
|
|
833
|
+
const lineContent = diffLine.content || '';
|
|
834
|
+
const lineNum = diffLine.newLine || diffLine.oldLine;
|
|
835
|
+
|
|
836
|
+
comments.push({
|
|
837
|
+
file: currentFile,
|
|
838
|
+
diffLineIndex: diffLineIndex,
|
|
839
|
+
line: lineNum,
|
|
840
|
+
lineContent: lineContent,
|
|
841
|
+
selectedText: selectedText,
|
|
842
|
+
text: text,
|
|
843
|
+
matchType: 'exact', // New comments are exact matches
|
|
844
|
+
followUps: [] // Initialize empty follow-ups array
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// Auto-save to backend
|
|
848
|
+
saveCommentsToBackend();
|
|
849
|
+
|
|
850
|
+
// Update comments sidebar
|
|
851
|
+
updateCommentsSidebar();
|
|
852
|
+
|
|
853
|
+
// Reload the current file to show the new comment
|
|
854
|
+
const fileIndex = currentFiles.findIndex(f => f.path === currentFile);
|
|
855
|
+
loadFile(currentFile, fileIndex);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Edit comment
|
|
859
|
+
function editComment(diffLineIndex, selectedText = null) {
|
|
860
|
+
const comment = comments.find(c => c.file === currentFile && c.diffLineIndex === diffLineIndex);
|
|
861
|
+
if (!comment) return;
|
|
862
|
+
|
|
863
|
+
// Remove the comment from the list temporarily
|
|
864
|
+
comments = comments.filter(c => !(c.file === currentFile && c.diffLineIndex === diffLineIndex));
|
|
865
|
+
|
|
866
|
+
// Reload file and show edit input
|
|
867
|
+
const fileIndex = currentFiles.findIndex(f => f.path === currentFile);
|
|
868
|
+
loadFile(currentFile, fileIndex).then(() => {
|
|
869
|
+
// Show comment input with existing text
|
|
870
|
+
const lineEl = document.querySelector(`.line[data-diff-index="${diffLineIndex}"]`);
|
|
871
|
+
if (!lineEl) return;
|
|
872
|
+
|
|
873
|
+
const inputBox = document.createElement('div');
|
|
874
|
+
inputBox.className = 'comment-input-box';
|
|
875
|
+
|
|
876
|
+
const selectedTextHtml = selectedText
|
|
877
|
+
? `<div class="selected-text-preview">
|
|
878
|
+
<strong>Selected code:</strong>
|
|
879
|
+
<pre>${escapeHtml(selectedText)}</pre>
|
|
880
|
+
</div>`
|
|
881
|
+
: '';
|
|
882
|
+
|
|
883
|
+
inputBox.innerHTML = `
|
|
884
|
+
${selectedTextHtml}
|
|
885
|
+
<textarea placeholder="Enter your comment (Cmd/Ctrl+Enter to save)..." id="commentInput">${escapeHtml(comment.text)}</textarea>
|
|
886
|
+
<div class="actions">
|
|
887
|
+
<button onclick="saveComment(${diffLineIndex}, ${selectedText ? `\`${escapeHtml(selectedText).replace(/`/g, '\\`')}\`` : 'null'})">Save Comment</button>
|
|
888
|
+
<button class="cancel-btn" onclick="this.closest('.comment-input-box').remove()">Cancel</button>
|
|
889
|
+
</div>
|
|
890
|
+
`;
|
|
891
|
+
|
|
892
|
+
lineEl.after(inputBox);
|
|
893
|
+
const textarea = document.getElementById('commentInput');
|
|
894
|
+
textarea.focus();
|
|
895
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
896
|
+
|
|
897
|
+
// Auto-resize textarea
|
|
898
|
+
const autoResize = () => {
|
|
899
|
+
textarea.style.height = 'auto';
|
|
900
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
textarea.addEventListener('input', autoResize);
|
|
904
|
+
autoResize(); // Initial resize
|
|
905
|
+
|
|
906
|
+
// Add keyboard shortcuts
|
|
907
|
+
textarea.addEventListener('keydown', (e) => {
|
|
908
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
909
|
+
e.preventDefault();
|
|
910
|
+
saveComment(diffLineIndex, selectedText);
|
|
911
|
+
}
|
|
912
|
+
if (e.key === 'Escape') {
|
|
913
|
+
e.preventDefault();
|
|
914
|
+
inputBox.remove();
|
|
915
|
+
// Restore the comment if canceled
|
|
916
|
+
comments.push(comment);
|
|
917
|
+
const fileIndex = currentFiles.findIndex(f => f.path === currentFile);
|
|
918
|
+
loadFile(currentFile, fileIndex);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Delete comment
|
|
925
|
+
function deleteComment(diffLineIndex) {
|
|
926
|
+
comments = comments.filter(c => !(c.file === currentFile && c.diffLineIndex === diffLineIndex));
|
|
927
|
+
|
|
928
|
+
// Auto-save to backend
|
|
929
|
+
saveCommentsToBackend();
|
|
930
|
+
|
|
931
|
+
// Update comments sidebar
|
|
932
|
+
updateCommentsSidebar();
|
|
933
|
+
|
|
934
|
+
// Reload the current file to remove the comment
|
|
935
|
+
const fileIndex = currentFiles.findIndex(f => f.path === currentFile);
|
|
936
|
+
loadFile(currentFile, fileIndex);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Submit review
|
|
940
|
+
async function submitReview() {
|
|
941
|
+
if (!currentRepoId) {
|
|
942
|
+
showStatus('Please load a repository first', 'error');
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Ensure comments are saved before generating review
|
|
947
|
+
await saveCommentsToBackend();
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
const response = await fetch(`${API_BASE}/submit-review`, {
|
|
951
|
+
method: 'POST',
|
|
952
|
+
headers: { 'Content-Type': 'application/json' },
|
|
953
|
+
body: JSON.stringify({
|
|
954
|
+
repoId: currentRepoId
|
|
955
|
+
// Backend reads from JSON file (single source of truth)
|
|
956
|
+
})
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const data = await response.json();
|
|
960
|
+
|
|
961
|
+
if (!response.ok) {
|
|
962
|
+
throw new Error(data.error || 'Failed to submit review');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
showReviewModal(data.reviewContent, data.filename);
|
|
966
|
+
showStatus(`Review submitted successfully! ${data.totalComments} comments`, 'success');
|
|
967
|
+
|
|
968
|
+
} catch (error) {
|
|
969
|
+
showStatus(`Error: ${error.message}`, 'error');
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Show review modal with download and copy options
|
|
974
|
+
function showReviewModal(reviewContent, filename) {
|
|
975
|
+
// Create modal overlay
|
|
976
|
+
const modal = document.createElement('div');
|
|
977
|
+
modal.className = 'review-modal';
|
|
978
|
+
modal.innerHTML = `
|
|
979
|
+
<div class="review-modal-content">
|
|
980
|
+
<div class="review-modal-header">
|
|
981
|
+
<h2>Review Submitted</h2>
|
|
982
|
+
<button class="close-modal" onclick="this.closest('.review-modal').remove()">×</button>
|
|
983
|
+
</div>
|
|
984
|
+
<div class="review-modal-body">
|
|
985
|
+
<pre class="review-text">${escapeHtml(reviewContent)}</pre>
|
|
986
|
+
</div>
|
|
987
|
+
<div class="review-modal-footer">
|
|
988
|
+
<button onclick="downloadReview('${filename}', this.closest('.review-modal').querySelector('.review-text').textContent)">
|
|
989
|
+
Download ${filename}
|
|
990
|
+
</button>
|
|
991
|
+
<button onclick="copyReviewToClipboard(this.closest('.review-modal').querySelector('.review-text').textContent)">
|
|
992
|
+
Copy to Clipboard
|
|
993
|
+
</button>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
`;
|
|
997
|
+
|
|
998
|
+
document.body.appendChild(modal);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Download review as file
|
|
1002
|
+
function downloadReview(filename, content) {
|
|
1003
|
+
const blob = new Blob([content], { type: 'text/plain' });
|
|
1004
|
+
const url = URL.createObjectURL(blob);
|
|
1005
|
+
const a = document.createElement('a');
|
|
1006
|
+
a.href = url;
|
|
1007
|
+
a.download = filename;
|
|
1008
|
+
a.click();
|
|
1009
|
+
URL.revokeObjectURL(url);
|
|
1010
|
+
showStatus('Review downloaded successfully', 'success');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Copy review to clipboard
|
|
1014
|
+
async function copyReviewToClipboard(content) {
|
|
1015
|
+
try {
|
|
1016
|
+
await navigator.clipboard.writeText(content);
|
|
1017
|
+
showStatus('Review copied to clipboard!', 'success');
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
showStatus('Failed to copy to clipboard', 'error');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Cleanup session
|
|
1024
|
+
async function cleanupRepo() {
|
|
1025
|
+
if (!currentRepoId) return;
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
await fetch(`${API_BASE}/cleanup/${currentRepoId}`, {
|
|
1029
|
+
method: 'DELETE'
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
showStatus('Session cleared', 'success');
|
|
1033
|
+
resetApp();
|
|
1034
|
+
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
console.error('Cleanup error:', error);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Reset application
|
|
1041
|
+
function resetApp() {
|
|
1042
|
+
currentRepoId = null;
|
|
1043
|
+
currentFiles = [];
|
|
1044
|
+
currentFile = null;
|
|
1045
|
+
currentFileIndex = -1;
|
|
1046
|
+
comments = [];
|
|
1047
|
+
|
|
1048
|
+
document.getElementById('repoPath').value = '';
|
|
1049
|
+
document.getElementById('sidebar').classList.add('hidden');
|
|
1050
|
+
document.getElementById('resizeHandle').classList.add('hidden');
|
|
1051
|
+
document.getElementById('codeSection').classList.add('hidden');
|
|
1052
|
+
document.getElementById('commentsSidebar').classList.add('hidden');
|
|
1053
|
+
document.getElementById('submitReviewBtn').classList.add('hidden');
|
|
1054
|
+
document.getElementById('status').textContent = '';
|
|
1055
|
+
document.getElementById('status').className = 'status';
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Show status message
|
|
1059
|
+
function showStatus(message, type) {
|
|
1060
|
+
const statusEl = document.getElementById('status');
|
|
1061
|
+
statusEl.textContent = 'Status: ' + message;
|
|
1062
|
+
statusEl.className = `status ${type}`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Track if we're in full context mode
|
|
1066
|
+
let isFullContextMode = false;
|
|
1067
|
+
let storedFullFileLines = null;
|
|
1068
|
+
|
|
1069
|
+
// Toggle between full context and diff-only view
|
|
1070
|
+
async function showFullContext() {
|
|
1071
|
+
if (!currentRepoId || !currentFile) return;
|
|
1072
|
+
|
|
1073
|
+
if (isFullContextMode) {
|
|
1074
|
+
// Switch back to diff view
|
|
1075
|
+
displayCode(currentFile, currentDiffLines);
|
|
1076
|
+
isFullContextMode = false;
|
|
1077
|
+
updateFullContextButton();
|
|
1078
|
+
} else {
|
|
1079
|
+
// Switch to full context
|
|
1080
|
+
try {
|
|
1081
|
+
const response = await fetch(`${API_BASE}/file-full/${currentRepoId}/${currentFile}`);
|
|
1082
|
+
const data = await response.json();
|
|
1083
|
+
|
|
1084
|
+
if (!response.ok) {
|
|
1085
|
+
throw new Error(data.error || 'Failed to load full file content');
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
storedFullFileLines = data.lines;
|
|
1089
|
+
displayFullContext(data.filePath, data.lines);
|
|
1090
|
+
isFullContextMode = true;
|
|
1091
|
+
updateFullContextButton();
|
|
1092
|
+
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
showStatus(`Error loading full context: ${error.message}`, 'error');
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Update the full context button text
|
|
1100
|
+
function updateFullContextButton() {
|
|
1101
|
+
const button = document.querySelector('.full-context-btn');
|
|
1102
|
+
if (button) {
|
|
1103
|
+
const icon = button.querySelector('svg');
|
|
1104
|
+
if (isFullContextMode) {
|
|
1105
|
+
button.innerHTML = icon.outerHTML + ' Show Diff Only';
|
|
1106
|
+
} else {
|
|
1107
|
+
button.innerHTML = icon.outerHTML + ' Full Context';
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Display full file content with deleted lines
|
|
1113
|
+
function displayFullContext(filePath, lines) {
|
|
1114
|
+
const codeViewer = document.getElementById('codeViewer');
|
|
1115
|
+
const fileComments = comments.filter(c => c.file === filePath);
|
|
1116
|
+
|
|
1117
|
+
// Create a merged view: currentDiffLines + missing lines from full content
|
|
1118
|
+
const mergedLines = [];
|
|
1119
|
+
const coveredNewLines = new Set();
|
|
1120
|
+
|
|
1121
|
+
// First, add all diff lines (includes deleted, added, and changed lines)
|
|
1122
|
+
let lastNewLine = 0;
|
|
1123
|
+
currentDiffLines.forEach(diffLine => {
|
|
1124
|
+
// Check if we need to fill in any missing lines before this diffLine
|
|
1125
|
+
if (diffLine.newLine && diffLine.newLine > lastNewLine + 1) {
|
|
1126
|
+
// Fill in the gap with unchanged lines
|
|
1127
|
+
for (let i = lastNewLine + 1; i < diffLine.newLine; i++) {
|
|
1128
|
+
if (lines[i - 1] !== undefined) {
|
|
1129
|
+
mergedLines.push({
|
|
1130
|
+
oldLine: i,
|
|
1131
|
+
newLine: i,
|
|
1132
|
+
type: 'unchanged',
|
|
1133
|
+
content: lines[i - 1]
|
|
1134
|
+
});
|
|
1135
|
+
coveredNewLines.add(i);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
mergedLines.push(diffLine);
|
|
1141
|
+
if (diffLine.newLine) {
|
|
1142
|
+
coveredNewLines.add(diffLine.newLine);
|
|
1143
|
+
lastNewLine = diffLine.newLine;
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// Add any remaining lines after the last diff
|
|
1148
|
+
for (let i = lastNewLine + 1; i <= lines.length; i++) {
|
|
1149
|
+
if (!coveredNewLines.has(i) && lines[i - 1] !== undefined) {
|
|
1150
|
+
mergedLines.push({
|
|
1151
|
+
oldLine: i,
|
|
1152
|
+
newLine: i,
|
|
1153
|
+
type: 'unchanged',
|
|
1154
|
+
content: lines[i - 1]
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Find matching diffLineIndex for comments in full context
|
|
1160
|
+
// Need to map mergedLines back to original diffLines indices
|
|
1161
|
+
const mergedToDiffIndexMap = new Map();
|
|
1162
|
+
mergedLines.forEach((mergedLine, mergedIndex) => {
|
|
1163
|
+
// Find this line in original diffLines
|
|
1164
|
+
const diffIndex = currentDiffLines.findIndex(dl =>
|
|
1165
|
+
(dl.newLine === mergedLine.newLine && dl.oldLine === mergedLine.oldLine && dl.content === mergedLine.content)
|
|
1166
|
+
);
|
|
1167
|
+
if (diffIndex !== -1) {
|
|
1168
|
+
mergedToDiffIndexMap.set(mergedIndex, diffIndex);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Now render the merged lines
|
|
1173
|
+
let html = '';
|
|
1174
|
+
mergedLines.forEach((diffLine, mergedIndex) => {
|
|
1175
|
+
const diffLineIndex = mergedToDiffIndexMap.get(mergedIndex);
|
|
1176
|
+
const hasComment = diffLineIndex !== undefined ? fileComments.find(c => c.diffLineIndex === diffLineIndex) : null;
|
|
1177
|
+
|
|
1178
|
+
const lineClass = `line diff-${diffLine.type} ${hasComment ? 'commented' : ''}`;
|
|
1179
|
+
const dataDiffIndexAttr = diffLineIndex !== undefined ? `data-diff-index="${diffLineIndex}"` : '';
|
|
1180
|
+
|
|
1181
|
+
html += `
|
|
1182
|
+
<div class="${lineClass}" ${dataDiffIndexAttr}>
|
|
1183
|
+
${hasComment ? '<span class="comment-indicator"></span>' : ''}
|
|
1184
|
+
<div class="line-numbers">
|
|
1185
|
+
<span class="old-line-number">${diffLine.oldLine || ''}</span>
|
|
1186
|
+
<span class="new-line-number" ${diffLineIndex !== undefined ? `onclick="toggleCommentInput(${diffLineIndex})"` : ''}>${diffLine.newLine || ''}</span>
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="line-content">${escapeHtml(diffLine.content) || ' '}</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
`;
|
|
1191
|
+
|
|
1192
|
+
if (hasComment) {
|
|
1193
|
+
const selectedTextAttr = hasComment.selectedText ? `'${escapeHtml(hasComment.selectedText).replace(/'/g, "\\'")}'` : 'null';
|
|
1194
|
+
html += `
|
|
1195
|
+
<div class="comment-box">
|
|
1196
|
+
<span class="comment-text">${escapeHtml(hasComment.text)}</span>
|
|
1197
|
+
<div class="comment-actions">
|
|
1198
|
+
<button class="comment-edit" onclick="editComment(${diffLineIndex}, ${selectedTextAttr})">Edit</button>
|
|
1199
|
+
<button class="comment-delete" onclick="deleteComment(${diffLineIndex})">Delete</button>
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
`;
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
codeViewer.innerHTML = html;
|
|
1207
|
+
|
|
1208
|
+
// Add text selection handler for full context too
|
|
1209
|
+
codeViewer.removeEventListener('mousedown', trackShiftKeyDown);
|
|
1210
|
+
codeViewer.removeEventListener('mouseup', handleTextSelection);
|
|
1211
|
+
codeViewer.addEventListener('mousedown', trackShiftKeyDown);
|
|
1212
|
+
codeViewer.addEventListener('mouseup', handleTextSelection);
|
|
1213
|
+
|
|
1214
|
+
showStatus('Showing full file context', 'success');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Escape HTML
|
|
1218
|
+
function escapeHtml(text) {
|
|
1219
|
+
const div = document.createElement('div');
|
|
1220
|
+
div.textContent = text;
|
|
1221
|
+
return div.innerHTML;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Sidebar resize functionality
|
|
1225
|
+
function initResizeHandle() {
|
|
1226
|
+
const resizeHandle = document.getElementById('resizeHandle');
|
|
1227
|
+
const sidebar = document.getElementById('sidebar');
|
|
1228
|
+
const commentsResizeHandle = document.getElementById('commentsResizeHandle');
|
|
1229
|
+
const commentsSidebar = document.getElementById('commentsSidebar');
|
|
1230
|
+
let isResizing = false;
|
|
1231
|
+
let isResizingComments = false;
|
|
1232
|
+
|
|
1233
|
+
// Left sidebar resize
|
|
1234
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
1235
|
+
isResizing = true;
|
|
1236
|
+
resizeHandle.classList.add('resizing');
|
|
1237
|
+
document.body.style.cursor = 'col-resize';
|
|
1238
|
+
document.body.style.userSelect = 'none';
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Comments sidebar resize
|
|
1242
|
+
commentsResizeHandle.addEventListener('mousedown', (e) => {
|
|
1243
|
+
isResizingComments = true;
|
|
1244
|
+
commentsResizeHandle.classList.add('resizing');
|
|
1245
|
+
document.body.style.cursor = 'col-resize';
|
|
1246
|
+
document.body.style.userSelect = 'none';
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
document.addEventListener('mousemove', (e) => {
|
|
1250
|
+
if (isResizing) {
|
|
1251
|
+
const newWidth = e.clientX;
|
|
1252
|
+
const minWidth = 200;
|
|
1253
|
+
const maxWidth = 600;
|
|
1254
|
+
|
|
1255
|
+
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
|
1256
|
+
sidebar.style.width = `${newWidth}px`;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (isResizingComments) {
|
|
1261
|
+
const newWidth = window.innerWidth - e.clientX;
|
|
1262
|
+
const minWidth = 250;
|
|
1263
|
+
const maxWidth = 600;
|
|
1264
|
+
|
|
1265
|
+
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
|
1266
|
+
commentsSidebar.style.width = `${newWidth}px`;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
document.addEventListener('mouseup', () => {
|
|
1272
|
+
if (isResizing) {
|
|
1273
|
+
isResizing = false;
|
|
1274
|
+
resizeHandle.classList.remove('resizing');
|
|
1275
|
+
document.body.style.cursor = '';
|
|
1276
|
+
document.body.style.userSelect = '';
|
|
1277
|
+
}
|
|
1278
|
+
if (isResizingComments) {
|
|
1279
|
+
isResizingComments = false;
|
|
1280
|
+
commentsResizeHandle.classList.remove('resizing');
|
|
1281
|
+
document.body.style.cursor = '';
|
|
1282
|
+
document.body.style.userSelect = '';
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Track current file index for keyboard navigation
|
|
1288
|
+
let currentFileIndex = -1;
|
|
1289
|
+
|
|
1290
|
+
// Update comments sidebar
|
|
1291
|
+
function updateCommentsSidebar() {
|
|
1292
|
+
const sidebar = document.getElementById('commentsSidebar');
|
|
1293
|
+
const commentsList = document.getElementById('commentsList');
|
|
1294
|
+
const commentsCount = document.getElementById('commentsCount');
|
|
1295
|
+
const commentsResizeHandle = document.getElementById('commentsResizeHandle');
|
|
1296
|
+
|
|
1297
|
+
commentsCount.textContent = comments.length;
|
|
1298
|
+
|
|
1299
|
+
if (comments.length === 0) {
|
|
1300
|
+
sidebar.classList.add('hidden');
|
|
1301
|
+
commentsResizeHandle.classList.add('hidden');
|
|
1302
|
+
commentsList.innerHTML = '<div style="text-align: center; color: #586069; padding: 20px; font-size: 13px;">No comments yet</div>';
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
sidebar.classList.remove('hidden');
|
|
1307
|
+
commentsResizeHandle.classList.remove('hidden');
|
|
1308
|
+
|
|
1309
|
+
// Group comments by file
|
|
1310
|
+
const commentsByFile = {};
|
|
1311
|
+
comments.forEach(comment => {
|
|
1312
|
+
if (!commentsByFile[comment.file]) {
|
|
1313
|
+
commentsByFile[comment.file] = [];
|
|
1314
|
+
}
|
|
1315
|
+
commentsByFile[comment.file].push(comment);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
let html = '';
|
|
1319
|
+
for (const [file, fileComments] of Object.entries(commentsByFile)) {
|
|
1320
|
+
fileComments
|
|
1321
|
+
.sort((a, b) => a.line - b.line)
|
|
1322
|
+
.forEach(comment => {
|
|
1323
|
+
const selectedHtml = comment.selectedText
|
|
1324
|
+
? `<div class="comment-item-selected">${escapeHtml(comment.selectedText)}</div>`
|
|
1325
|
+
: '';
|
|
1326
|
+
|
|
1327
|
+
const fileEscaped = file.replace(/'/g, "\\'");
|
|
1328
|
+
html += `
|
|
1329
|
+
<div class="comment-item">
|
|
1330
|
+
<button class="comment-item-delete" onclick="event.stopPropagation(); deleteCommentFromSidebar('${fileEscaped}', ${comment.diffLineIndex})" title="Delete comment">×</button>
|
|
1331
|
+
<div class="comment-item-content" onclick="jumpToComment('${fileEscaped}', ${comment.diffLineIndex})">
|
|
1332
|
+
<div class="comment-item-file">${escapeHtml(file)}</div>
|
|
1333
|
+
<div class="comment-item-line">Line ${comment.line}</div>
|
|
1334
|
+
<div class="comment-item-text">${escapeHtml(comment.text)}</div>
|
|
1335
|
+
${selectedHtml}
|
|
1336
|
+
</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
`;
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
commentsList.innerHTML = html;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Delete comment from sidebar
|
|
1346
|
+
function deleteCommentFromSidebar(file, diffLineIndex) {
|
|
1347
|
+
comments = comments.filter(c => !(c.file === file && c.diffLineIndex === diffLineIndex));
|
|
1348
|
+
|
|
1349
|
+
// Auto-save to backend
|
|
1350
|
+
saveCommentsToBackend();
|
|
1351
|
+
|
|
1352
|
+
// Update comments sidebar
|
|
1353
|
+
updateCommentsSidebar();
|
|
1354
|
+
|
|
1355
|
+
// If we're currently viewing this file, reload it to remove the comment
|
|
1356
|
+
if (currentFile === file) {
|
|
1357
|
+
const fileIndex = currentFiles.findIndex(f => f.path === file);
|
|
1358
|
+
loadFile(file, fileIndex);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Jump to a specific comment
|
|
1363
|
+
function jumpToComment(file, diffLineIndex) {
|
|
1364
|
+
// Find the file index
|
|
1365
|
+
const fileIndex = currentFiles.findIndex(f => f.path === file);
|
|
1366
|
+
if (fileIndex === -1) return;
|
|
1367
|
+
|
|
1368
|
+
// Load the file
|
|
1369
|
+
loadFile(file, fileIndex).then(() => {
|
|
1370
|
+
// Scroll to the comment
|
|
1371
|
+
const lineEl = document.querySelector(`.line[data-diff-index="${diffLineIndex}"]`);
|
|
1372
|
+
if (lineEl) {
|
|
1373
|
+
lineEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1374
|
+
// Highlight briefly
|
|
1375
|
+
lineEl.style.backgroundColor = 'rgba(3, 102, 214, 0.1)';
|
|
1376
|
+
setTimeout(() => {
|
|
1377
|
+
lineEl.style.backgroundColor = '';
|
|
1378
|
+
}, 1000);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Navigate to next/previous file
|
|
1384
|
+
function navigateFiles(direction) {
|
|
1385
|
+
if (currentFiles.length === 0) return;
|
|
1386
|
+
|
|
1387
|
+
let newIndex = currentFileIndex;
|
|
1388
|
+
|
|
1389
|
+
// If no file is loaded yet, load the first file
|
|
1390
|
+
if (currentFileIndex === -1) {
|
|
1391
|
+
newIndex = 0;
|
|
1392
|
+
} else if (direction === 'up') {
|
|
1393
|
+
newIndex = currentFileIndex > 0 ? currentFileIndex - 1 : 0;
|
|
1394
|
+
} else if (direction === 'down') {
|
|
1395
|
+
newIndex = currentFileIndex < currentFiles.length - 1 ? currentFileIndex + 1 : currentFiles.length - 1;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (newIndex !== currentFileIndex) {
|
|
1399
|
+
currentFileIndex = newIndex;
|
|
1400
|
+
const file = currentFiles[newIndex].path;
|
|
1401
|
+
loadFile(file, newIndex);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Allow Enter key to load repo
|
|
1406
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1407
|
+
document.getElementById('repoPath').addEventListener('keypress', (e) => {
|
|
1408
|
+
if (e.key === 'Enter') {
|
|
1409
|
+
loadRepo();
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
// Initialize resize handle
|
|
1414
|
+
initResizeHandle();
|
|
1415
|
+
|
|
1416
|
+
// Add keyboard navigation for files
|
|
1417
|
+
document.addEventListener('keydown', (e) => {
|
|
1418
|
+
// Only handle arrow keys when not in an input/textarea
|
|
1419
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (e.key === 'ArrowUp') {
|
|
1424
|
+
e.preventDefault();
|
|
1425
|
+
navigateFiles('up');
|
|
1426
|
+
} else if (e.key === 'ArrowDown') {
|
|
1427
|
+
e.preventDefault();
|
|
1428
|
+
navigateFiles('down');
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// `reviewer /path/to/repo` opens the page with the repository in the query
|
|
1433
|
+
// string, so the review is on screen without anyone typing a path.
|
|
1434
|
+
const requestedRepo = new URLSearchParams(window.location.search).get('repo');
|
|
1435
|
+
if (requestedRepo) {
|
|
1436
|
+
document.getElementById('repoPath').value = requestedRepo;
|
|
1437
|
+
loadRepo();
|
|
1438
|
+
}
|
|
1439
|
+
});
|