@@ -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
563871document . addEventListener ( 'DOMContentLoaded' , function ( ) {
0 commit comments