Skip to content

Commit c64f87a

Browse files
committed
Refactor review submission process to enhance GitHub integration
Updated the submitReview function to be asynchronous, allowing for the addition of reviews directly to the GitHub repository via the GitHub API. Introduced error handling for GitHub submission failures and improved user feedback for local storage saves. Renamed and modified the downloadReviewJSONForSubmission function to addReviewToReviewsFolder, which now handles file creation and updates on GitHub.
1 parent cdbc163 commit c64f87a

File tree

1 file changed

+319
-11
lines changed

1 file changed

+319
-11
lines changed

docs/script.js

Lines changed: 319 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ async function loadReviewsFromJSON() {
489489
}
490490

491491
// Submit review form
492-
function submitReview(event, methodName) {
492+
async function submitReview(event, methodName) {
493493
event.preventDefault();
494494

495495
const form = document.getElementById(`review-form-${methodName}`);
@@ -511,13 +511,17 @@ function submitReview(event, methodName) {
511511
reviews.push(review);
512512
saveReviewsToStorage(reviews);
513513

514-
// Automatically download JSON file for the review
515-
downloadReviewJSONForSubmission(review);
516-
517-
// Show success message
518-
const successDiv = document.getElementById(`review-success-${methodName}`);
519-
if (successDiv) {
520-
successDiv.style.display = 'block';
514+
// Automatically add JSON file to reviews folder via GitHub API
515+
try {
516+
await addReviewToReviewsFolder(review);
517+
// Show success message
518+
const successDiv = document.getElementById(`review-success-${methodName}`);
519+
if (successDiv) {
520+
successDiv.style.display = 'block';
521+
}
522+
} catch (error) {
523+
console.error('Error adding review to reviews folder:', error);
524+
alert('Review saved locally. Could not add to reviews folder. Please use "Download Review as JSON" and commit manually.');
521525
}
522526

523527
// Reset form
@@ -534,8 +538,8 @@ function submitReview(event, methodName) {
534538
return false;
535539
}
536540

537-
// Download review as JSON file for submission to /reviews/ directory
538-
function downloadReviewJSONForSubmission(review) {
541+
// Add review JSON file to reviews/ folder via GitHub API
542+
async function addReviewToReviewsFolder(review) {
539543
// Ensure all required fields are present
540544
const reviewObject = {
541545
id: review.id || Date.now().toString(),
@@ -548,16 +552,320 @@ function downloadReviewJSONForSubmission(review) {
548552
timestamp: review.timestamp
549553
};
550554

555+
// Create JSON content for the file
556+
const jsonContent = JSON.stringify(reviewObject, null, 2);
557+
const fileName = `review_${review.method}_${review.id}.json`;
558+
const filePath = `reviews/${fileName}`;
559+
560+
// GitHub repository info
561+
const repoOwner = 'SingularityNET-Archive';
562+
const repoName = 'Graph-Python-scripts';
563+
const branch = 'main';
564+
565+
// Get GitHub token from localStorage or prompt user
566+
let githubToken = localStorage.getItem('github_token');
567+
568+
if (!githubToken) {
569+
// Prompt for token
570+
githubToken = prompt('GitHub Personal Access Token required to add file to reviews folder.\n\nPlease enter your token (starts with ghp_):');
571+
if (!githubToken || !githubToken.startsWith('ghp_')) {
572+
throw new Error('Valid GitHub token required');
573+
}
574+
// Verify token and store it
575+
try {
576+
const userResponse = await fetch('https://api.github.com/user', {
577+
headers: {
578+
'Authorization': `token ${githubToken}`,
579+
'Accept': 'application/vnd.github.v3+json'
580+
}
581+
});
582+
if (!userResponse.ok) {
583+
throw new Error('Invalid token');
584+
}
585+
localStorage.setItem('github_token', githubToken);
586+
} catch (error) {
587+
throw new Error('Invalid GitHub token. Please try again.');
588+
}
589+
}
590+
591+
// Encode file content to base64
592+
const base64Content = btoa(unescape(encodeURIComponent(jsonContent)));
593+
594+
try {
595+
// Get the current file SHA (if it exists) for update
596+
const apiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}`;
597+
598+
// First, check if file exists
599+
let fileSha = null;
600+
try {
601+
const response = await fetch(`${apiUrl}?ref=${branch}`, {
602+
headers: {
603+
'Authorization': `token ${githubToken}`,
604+
'Accept': 'application/vnd.github.v3+json'
605+
}
606+
});
607+
if (response.ok) {
608+
const data = await response.json();
609+
fileSha = data.sha;
610+
}
611+
} catch (e) {
612+
// File doesn't exist, which is fine for new files
613+
}
614+
615+
// Create or update the file
616+
const createResponse = await fetch(apiUrl, {
617+
method: fileSha ? 'PUT' : 'POST',
618+
headers: {
619+
'Authorization': `token ${githubToken}`,
620+
'Accept': 'application/vnd.github.v3+json',
621+
'Content-Type': 'application/json'
622+
},
623+
body: JSON.stringify({
624+
message: `Add review for ${review.method} analysis [skip ci]`,
625+
content: base64Content,
626+
branch: branch,
627+
...(fileSha ? { sha: fileSha } : {})
628+
})
629+
});
630+
631+
if (!createResponse.ok) {
632+
const errorData = await createResponse.json();
633+
// If unauthorized, clear token and ask to re-enter
634+
if (createResponse.status === 401) {
635+
localStorage.removeItem('github_token');
636+
throw new Error('Authentication expired. Please refresh and try again.');
637+
}
638+
throw new Error(errorData.message || 'Failed to create file on GitHub');
639+
}
640+
641+
const result = await createResponse.json();
642+
console.log('Review file added to reviews folder successfully:', result.content.html_url);
643+
return true;
644+
645+
} catch (error) {
646+
console.error('Error adding file to reviews folder:', error);
647+
throw error;
648+
}
649+
}
650+
651+
// Display reviews for a specific method
652+
function displayReviewsForMethod(methodName) {
653+
const reviewsList = document.getElementById(`reviews-list-${methodName}`);
654+
if (!reviewsList) return;
655+
656+
const allReviews = loadReviewsFromStorage();
657+
const methodReviews = allReviews.filter(r => r.method === methodName);
658+
659+
if (methodReviews.length === 0) {
660+
reviewsList.innerHTML = '<p style="color: #586069; font-size: 0.9em;">No reviews yet. Be the first to submit a review!</p>';
661+
return;
662+
}
663+
664+
reviewsList.innerHTML = '<h4>Previous Reviews</h4>' + methodReviews.map(review => {
665+
const date = new Date(review.timestamp).toLocaleString();
666+
return `
667+
<div class="review-item rating-${review.rating}">
668+
<div class="review-item-header">
669+
<span class="review-item-rating rating-${review.rating}">${review.rating.toUpperCase()}</span>
670+
<span class="review-item-meta">${review.reviewer}${date}</span>
671+
</div>
672+
<div class="review-item-comment">${escapeHtml(review.comment)}</div>
673+
${review.suggestions ? `<div class="review-item-suggestions"><strong>Suggestions:</strong> ${escapeHtml(review.suggestions)}</div>` : ''}
674+
</div>
675+
`;
676+
}).join('');
677+
}
678+
679+
// Escape HTML to prevent XSS
680+
function escapeHtml(text) {
681+
const div = document.createElement('div');
682+
div.textContent = text;
683+
return div.innerHTML;
684+
}
685+
686+
// Download review as JSON (single review object format for GitHub workflow)
687+
function downloadReviewJSON(methodName) {
688+
const allReviews = loadReviewsFromStorage();
689+
const methodReviews = allReviews.filter(r => r.method === methodName);
690+
691+
if (methodReviews.length === 0) {
692+
alert('No reviews to download for this method.');
693+
return;
694+
}
695+
696+
// Get the most recent review (or allow user to select one)
697+
// For now, download the most recent review as a single object
698+
const latestReview = methodReviews.sort((a, b) =>
699+
new Date(b.timestamp) - new Date(a.timestamp)
700+
)[0];
701+
702+
// Ensure all required fields are present
703+
const reviewObject = {
704+
id: latestReview.id || Date.now().toString(),
705+
method: latestReview.method,
706+
rating: latestReview.rating,
707+
comment: latestReview.comment || '',
708+
reviewer: latestReview.reviewer || 'Anonymous',
709+
suggestions: latestReview.suggestions || '',
710+
file: latestReview.file || 'docs/index.html',
711+
timestamp: latestReview.timestamp
712+
};
713+
551714
const dataStr = JSON.stringify(reviewObject, null, 2);
552715
const dataBlob = new Blob([dataStr], { type: 'application/json' });
553716
const url = URL.createObjectURL(dataBlob);
554717
const link = document.createElement('a');
555718
link.href = url;
556-
link.download = `review_${review.method}_${review.id}.json`;
719+
link.download = `review_${methodName}_${latestReview.id || Date.now()}.json`;
557720
link.click();
558721
URL.revokeObjectURL(url);
559722
}
560723

724+
// Load and display reviews when tab is shown
725+
function loadReviewsForTab(methodName) {
726+
displayReviewsForMethod(methodName);
727+
}
728+
729+
// Update audit tab to show reviews
730+
async function loadAuditData() {
731+
const auditTab = document.getElementById('audit');
732+
if (!auditTab || !auditTab.classList.contains('active')) {
733+
return;
734+
}
735+
736+
// Load from localStorage
737+
const localReviews = loadReviewsFromStorage();
738+
739+
// Load from JSON file
740+
const jsonData = await loadReviewsFromJSON();
741+
742+
// Combine both sources
743+
const allReviews = [...localReviews];
744+
if (jsonData.methods) {
745+
Object.keys(jsonData.methods).forEach(method => {
746+
if (jsonData.methods[method].reviews) {
747+
jsonData.methods[method].reviews.forEach(review => {
748+
// Avoid duplicates
749+
if (!allReviews.find(r => r.id === review.id)) {
750+
allReviews.push(review);
751+
}
752+
});
753+
}
754+
});
755+
}
756+
757+
// Group by method
758+
const reviewsByMethod = {};
759+
const methodStats = {};
760+
761+
['coattendance', 'field-degree', 'path-structure', 'centrality', 'clustering', 'components'].forEach(method => {
762+
const methodReviews = allReviews.filter(r => r.method === method);
763+
reviewsByMethod[method] = methodReviews;
764+
765+
const stats = {
766+
total: methodReviews.length,
767+
correct: methodReviews.filter(r => r.rating === 'correct').length,
768+
incorrect: methodReviews.filter(r => r.rating === 'incorrect').length,
769+
needs_review: methodReviews.filter(r => r.rating === 'needs-review').length,
770+
trust_score: 0
771+
};
772+
773+
if (stats.total > 0) {
774+
stats.trust_score = ((stats.correct - stats.incorrect) / stats.total + 1) / 2;
775+
}
776+
777+
methodStats[method] = stats;
778+
});
779+
780+
// Display audit data
781+
displayAuditData(methodStats, reviewsByMethod, jsonData.last_updated);
782+
}
783+
784+
// Display audit data in the audit tab
785+
function displayAuditData(methodStats, reviewsByMethod, lastUpdated) {
786+
const auditTab = document.getElementById('audit');
787+
if (!auditTab) return;
788+
789+
let html = '<h2>Community Review Audit</h2>';
790+
791+
if (lastUpdated) {
792+
html += `<p class="explanation">Last updated from JSON: ${new Date(lastUpdated).toLocaleString()}</p>`;
793+
}
794+
795+
html += '<div class="audit-stats">';
796+
Object.keys(methodStats).forEach(method => {
797+
const stats = methodStats[method];
798+
const methodName = method.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
799+
html += `
800+
<div class="method-stat-card">
801+
<h3>${methodName}</h3>
802+
<div class="stat-row">
803+
<span>Total Reviews:</span>
804+
<strong>${stats.total}</strong>
805+
</div>
806+
<div class="stat-row">
807+
<span>Trust Score:</span>
808+
<strong>${(stats.trust_score * 100).toFixed(1)}%</strong>
809+
</div>
810+
<div class="stat-row">
811+
<span>Ratings:</span>
812+
<span>✓ ${stats.correct} | ? ${stats.needs_review} | ✗ ${stats.incorrect}</span>
813+
</div>
814+
</div>
815+
`;
816+
});
817+
html += '</div>';
818+
819+
html += '<h3>All Reviews</h3>';
820+
Object.keys(reviewsByMethod).forEach(method => {
821+
const reviews = reviewsByMethod[method];
822+
if (reviews.length === 0) return;
823+
824+
const methodName = method.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
825+
html += `<h4>${methodName}</h4>`;
826+
html += reviews.map(review => {
827+
const date = new Date(review.timestamp).toLocaleString();
828+
return `
829+
<div class="review-item rating-${review.rating}">
830+
<div class="review-item-header">
831+
<span class="review-item-rating rating-${review.rating}">${review.rating.toUpperCase()}</span>
832+
<span class="review-item-meta">${review.reviewer}${date}</span>
833+
</div>
834+
<div class="review-item-comment">${escapeHtml(review.comment)}</div>
835+
${review.suggestions ? `<div class="review-item-suggestions"><strong>Suggestions:</strong> ${escapeHtml(review.suggestions)}</div>` : ''}
836+
</div>
837+
`;
838+
}).join('');
839+
});
840+
841+
auditTab.innerHTML = html;
842+
}
843+
844+
// Wrap the original showTab function to add review loading
845+
(function() {
846+
// Store original showTab function
847+
const originalShowTab = showTab;
848+
849+
// Override showTab to load reviews when switching tabs
850+
window.showTab = function(tabId) {
851+
// Call original showTab
852+
originalShowTab(tabId);
853+
854+
// Load reviews for the current tab
855+
setTimeout(() => {
856+
const validMethods = ['coattendance', 'field-degree', 'path-structure', 'centrality', 'clustering', 'components'];
857+
if (validMethods.includes(tabId)) {
858+
if (typeof loadReviewsForTab === 'function') {
859+
loadReviewsForTab(tabId);
860+
}
861+
} else if (tabId === 'audit') {
862+
if (typeof loadAuditData === 'function') {
863+
loadAuditData();
864+
}
865+
}
866+
}, 100);
867+
};
868+
})();
561869

562870
// Load reviews on page load
563871
document.addEventListener('DOMContentLoaded', function() {

0 commit comments

Comments
 (0)