Add comment and reply functionality with preview and notification templates

This commit is contained in:
pacnpal
2025-02-07 13:13:49 -05:00
parent 2c4d2daf34
commit 0e0ed01cee
30 changed files with 5153 additions and 383 deletions

View File

@@ -0,0 +1,274 @@
// Enhanced Diff Viewer Component
class DiffViewer {
constructor(options = {}) {
this.renderStrategy = options.renderStrategy || 'side-by-side';
this.syntaxHighlighters = new Map();
this.commentThreads = [];
this.container = null;
this.performance = {
startTime: null,
endTime: null
};
}
initialize(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container element with id "${containerId}" not found`);
}
this.setupSyntaxHighlighters();
}
setupSyntaxHighlighters() {
// Set up Prism.js or similar syntax highlighting library
this.syntaxHighlighters.set('text', this.plainTextHighlighter);
this.syntaxHighlighters.set('json', this.jsonHighlighter);
this.syntaxHighlighters.set('python', this.pythonHighlighter);
}
async render(diffData) {
this.performance.startTime = performance.now();
const { changes, metadata, navigation } = diffData;
const content = this.renderStrategy === 'side-by-side'
? this.renderSideBySide(changes)
: this.renderInline(changes);
this.container.innerHTML = `
<div class="diff-viewer ${this.renderStrategy}">
<div class="diff-header">
${this.renderMetadata(metadata)}
${this.renderControls()}
</div>
<div class="diff-content">
${content}
</div>
${this.renderNavigation(navigation)}
</div>
`;
this.attachEventListeners();
await this.highlightSyntax();
this.performance.endTime = performance.now();
this.updatePerformanceMetrics();
}
renderSideBySide(changes) {
return Object.entries(changes).map(([field, change]) => `
<div class="diff-section" data-field="${field}">
<div class="diff-field-header">
<span class="field-name">${field}</span>
<span class="syntax-type">${change.syntax_type}</span>
</div>
<div class="diff-blocks">
<div class="diff-block old" data-anchor="${change.metadata.comment_anchor_id}-old">
<div class="line-numbers">
${this.renderLineNumbers(change.metadata.line_numbers.old)}
</div>
<pre><code class="language-${change.syntax_type}">${this.escapeHtml(change.old)}</code></pre>
</div>
<div class="diff-block new" data-anchor="${change.metadata.comment_anchor_id}-new">
<div class="line-numbers">
${this.renderLineNumbers(change.metadata.line_numbers.new)}
</div>
<pre><code class="language-${change.syntax_type}">${this.escapeHtml(change.new)}</code></pre>
</div>
</div>
</div>
`).join('');
}
renderInline(changes) {
return Object.entries(changes).map(([field, change]) => `
<div class="diff-section" data-field="${field}">
<div class="diff-field-header">
<span class="field-name">${field}</span>
<span class="syntax-type">${change.syntax_type}</span>
</div>
<div class="diff-block inline" data-anchor="${change.metadata.comment_anchor_id}">
<div class="line-numbers">
${this.renderLineNumbers(change.metadata.line_numbers.new)}
</div>
<pre><code class="language-${change.syntax_type}">
${this.renderInlineDiff(change.old, change.new)}
</code></pre>
</div>
</div>
`).join('');
}
renderMetadata(metadata) {
return `
<div class="diff-metadata">
<span class="timestamp">${new Date(metadata.timestamp).toLocaleString()}</span>
<span class="user">${metadata.user || 'Anonymous'}</span>
<span class="change-type">${this.formatChangeType(metadata.change_type)}</span>
${metadata.reason ? `<span class="reason">${metadata.reason}</span>` : ''}
</div>
`;
}
renderControls() {
return `
<div class="diff-controls">
<button class="btn-view-mode" data-mode="side-by-side">Side by Side</button>
<button class="btn-view-mode" data-mode="inline">Inline</button>
<button class="btn-collapse-all">Collapse All</button>
<button class="btn-expand-all">Expand All</button>
</div>
`;
}
renderNavigation(navigation) {
return `
<div class="diff-navigation">
${navigation.prev_id ? `<button class="btn-prev" data-id="${navigation.prev_id}">Previous</button>` : ''}
${navigation.next_id ? `<button class="btn-next" data-id="${navigation.next_id}">Next</button>` : ''}
<span class="position-indicator">Change ${navigation.current_position}</span>
</div>
`;
}
renderLineNumbers(numbers) {
return numbers.map(num => `<span class="line-number">${num}</span>`).join('');
}
renderInlineDiff(oldText, newText) {
// Simple inline diff implementation - could be enhanced with more sophisticated diff algorithm
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
const diffLines = [];
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
if (oldLines[i] !== newLines[i]) {
if (oldLines[i]) {
diffLines.push(`<span class="diff-removed">${this.escapeHtml(oldLines[i])}</span>`);
}
if (newLines[i]) {
diffLines.push(`<span class="diff-added">${this.escapeHtml(newLines[i])}</span>`);
}
} else if (oldLines[i]) {
diffLines.push(this.escapeHtml(oldLines[i]));
}
}
return diffLines.join('\n');
}
attachEventListeners() {
// View mode switching
this.container.querySelectorAll('.btn-view-mode').forEach(btn => {
btn.addEventListener('click', () => {
this.renderStrategy = btn.dataset.mode;
this.render(this.currentDiffData);
});
});
// Collapse/Expand functionality
this.container.querySelectorAll('.diff-section').forEach(section => {
section.querySelector('.diff-field-header').addEventListener('click', () => {
section.classList.toggle('collapsed');
});
});
// Navigation
this.container.querySelectorAll('.diff-navigation button').forEach(btn => {
btn.addEventListener('click', () => {
this.navigateToChange(btn.dataset.id);
});
});
}
async highlightSyntax() {
const codeBlocks = this.container.querySelectorAll('code[class^="language-"]');
for (const block of codeBlocks) {
const syntax = block.className.replace('language-', '');
const highlighter = this.syntaxHighlighters.get(syntax);
if (highlighter) {
await highlighter(block);
}
}
}
// Syntax highlighters
async plainTextHighlighter(element) {
// No highlighting needed for plain text
return element;
}
async jsonHighlighter(element) {
try {
const content = element.textContent;
const parsed = JSON.parse(content);
element.textContent = JSON.stringify(parsed, null, 2);
// Apply JSON syntax highlighting classes
element.innerHTML = element.innerHTML.replace(
/"([^"]+)":/g,
'<span class="json-key">"$1":</span>'
);
} catch (e) {
console.warn('JSON parsing failed:', e);
}
return element;
}
async pythonHighlighter(element) {
// Basic Python syntax highlighting
element.innerHTML = element.innerHTML
.replace(/(def|class|import|from|return|if|else|try|except)\b/g, '<span class="keyword">$1</span>')
.replace(/(["'])(.*?)\1/g, '<span class="string">$1$2$1</span>')
.replace(/#.*/g, '<span class="comment">$&</span>');
return element;
}
updatePerformanceMetrics() {
const renderTime = this.performance.endTime - this.performance.startTime;
if (renderTime > 200) { // Performance budget: 200ms
console.warn(`Diff render time (${renderTime}ms) exceeded performance budget`);
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatChangeType(type) {
const types = {
'C': 'Changed',
'D': 'Deleted',
'A': 'Added'
};
return types[type] || type;
}
addCommentThread(anchor, thread) {
this.commentThreads.push({ anchor, thread });
this.renderCommentThreads();
}
renderCommentThreads() {
this.commentThreads.forEach(({ anchor, thread }) => {
const element = this.container.querySelector(`[data-anchor="${anchor}"]`);
if (element) {
const threadElement = document.createElement('div');
threadElement.className = 'comment-thread';
threadElement.innerHTML = thread.map(comment => `
<div class="comment">
<div class="comment-header">
<span class="comment-author">${comment.author}</span>
<span class="comment-date">${new Date(comment.date).toLocaleString()}</span>
</div>
<div class="comment-content">${comment.content}</div>
</div>
`).join('');
element.appendChild(threadElement);
}
});
}
}
export default DiffViewer;