Jump to content

User:Asked42/উইকি নিরীক্ষণ.js

From Wikimedia Incubator

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* <nowiki>
* Name: Wiki Nirikshan (Translation: Wiki Reviewing)
* Inspiration: Multiple sources, mainly:
**   User:Syunsyunminmin/wikinews/easy-dialog.js,
**   User:EGardner (WMF)/codex-hello-world.js

* A user script for reviewing articles on Bangla Wikinews.
* Adds an option to the p-cactions for Vector 2022 skin and only to the p-tab for other skins.
*/

if (typeof wikiNirikshan === 'undefined') {
    var wikiNirikshan = {};
}
wikiNirikshan.api = new mw.Api();

const localization = {
    dialogTitle: 'উইকিসংবাদ নিবন্ধ নিরীক্ষা',
    closeButtonLabel: 'বন্ধ করুন',
    articleInfoTitle: 'নিবন্ধ সংক্রান্ত তথ্য',
    creatorLabel: 'লেখক:',
    creationDateLabel: 'তৈরির তারিখ:',
    totalEditsLabel: 'মোট সম্পাদনা:',
    lastEditorLabel: 'সর্বশেষ সম্পাদক:',
    talkLinkText: 'আলাপ',
    plagiarismCheckLabel: 'অনুলিপি পরীক্ষা:',
    plagiarismCheckLinkText: 'কপিরাইট লঙ্ঘন পরীক্ষা করুন',
    revisionNumberLabel: 'সংস্করণ সংখ্যা:',
    dateLabel: 'তারিখ:',
    reviewerLabel: 'নিরীক্ষক:',
    criteriaLabels: {
        copyright: 'কপিরাইট:',
        neutrality: 'নিরপেক্ষতা:',
        style: 'শৈলী:',
        content: 'বিষয়বস্তু:'
    },
    passLabel: 'সফল',
    failLabel: 'প্রস্তুত নয়',
    showSuggestedCommentsButton: 'পরামর্শকৃত মন্তব্য দেখুন',
    suggestedCommentsTitle: 'পরামর্শকৃত মন্তব্য',
    copyText: '(কপি)',
    commentsLabel: 'মন্তব্য:',
    tagAuthorLabel: 'লেখককে ট্যাগ করুন',
    submitReviewButton: 'জমা করুন',
    cancelButton: 'বাতিল',
    helpButton: 'সাহায্য',
    confirmDialogTitle: 'নিরীক্ষা জমা দেওয়ার নিশ্চিতকরণ',
    confirmDialogMessage: 'আপনি কি নিশ্চিত যে আপনি এই নিরীক্ষার ফলাফল জমা দিতে চান?',
    confirmSubmitButton: 'হ্যাঁ, জমা দিন',
    reviewResultLabel: 'আপনার নিরীক্ষণের ফলাফল:',
    successDialogTitle: 'নিরীক্ষা জমা দেওয়া হয়েছে',
    successDialogMessage: 'নিবন্ধ নিরীক্ষণের ফলাফল সফলভাবে জমা দেওয়া হয়েছে।',
    errorDialogMessage: 'নিরীক্ষণের প্রক্রিয়া চলাকালীন একটি ত্রুটি ঘটেছে।',
    okButton: 'ঠিক আছে',
    warningMessage: 'অনুগ্রহ করে "প্রস্তুত নয়" হিসেবে চিহ্নিত করার কারণ প্রদান করুন।',
    reviewPortletLink: 'নিরীক্ষা করুন',
    reviewPortletTitle: 'এই নিবন্ধটির নিরীক্ষা শুরু করুন',
    commentSuggestions: {
    positiveFeedback: [
        'নিবন্ধটি সমস্ত নিরপেক্ষতা এবং শৈলীর নির্দেশিকা মেনে চলে, এবং উৎসগুলি সঠিকভাবে উদ্ধৃত করা হয়েছে। ভালো কাজ!',
        'উৎসের যথাযথ ব্যবহারের জন্য আপনাকে ধন্যবাদ, যা নিবন্ধটির নির্ভরযোগ্যতাকে আরও বাড়িয়ে তুলেছে।',
        'প্রাসঙ্গিক এবং ভালভাবে সমর্থিত তথ্যের মাধ্যমে নিবন্ধটি একটি শক্তিশালী এবং বোধগম্য উপস্থাপনা প্রদান করে।',
        'নিবন্ধের তাত্ত্বিক বিশ্লেষণ অত্যন্ত সুনিপুণ। এটি বিষয়বস্তুর গভীরতা বাড়িয়েছে।',
        'শৈলী এবং বিন্যাসের দিক থেকে নিবন্ধটি পড়তে খুবই সহজ এবং উপভোগ্য ছিল।',

    ],
    constructiveFeedback: [
        'নিবন্ধের কিছু অংশ সরাসরি উৎস থেকে অনুলিপি করা হয়েছে বলে মনে হচ্ছে। দয়া করে এটি পুনর্লিখন করুন এবং আপনার নিজের ভাষায় তথ্য উপস্থাপন করুন।',
        'নিবন্ধের দ্বিতীয় পরিচ্ছেদের তথ্যগুলোর জন্য উদ্ধৃতির অভাব রয়েছে। দাবিগুলি শক্তিশালী করতে আরও নির্ভরযোগ্য উৎস যোগ করা গুরুত্বপূর্ণ।',
        'নিবন্ধের প্রথম পরিচ্ছেদে, ঘটনা সম্পর্কে কে, কি, কেন, কিভাবে এবং কারা ইত্যাদি প্রশ্নের উত্তর বর্ণনা করা হয়নি। নিবন্ধের প্রধান যুক্তিগুলি প্রতিফলিত করে একটি সারসংক্ষেপ যোগ করার কথা বিবেচনা করুন।',
        'অনুগ্রহ করে আরও ভালো স্পষ্টতার জন্য নিবন্ধটি সংশোধন করুন, বিশেষ করে জটিল তত্ত্বগুলি আলোচনা করা হয়েছে সেই অংশগুলিতে। বিষয়ের পূর্ব জ্ঞান ছাড়া কিছু ব্যাখ্যা অনুসরণ করা কঠিন।',
        'নিবন্ধে কিছু গুরুত্বপূর্ণ অংশ অসম্পূর্ণ মনে হয়েছে। বিষয়ের আরও গভীরতা এবং বিশ্লেষণ যুক্ত করলে এটি আরও সমৃদ্ধ হবে।',
        'নিবন্ধে নিরপেক্ষতার অভাব রয়েছে, বিশেষত যেখানে মতামতগুলো উপস্থাপিত হয়েছে। নিরপেক্ষভাবে বিষয়টি বিবেচনা করা প্রয়োজন।',
        'বিভিন্ন তথ্যের মধ্যে কিছু অসংগতির মতো মনে হচ্ছে। দয়া করে সংশ্লিষ্ট উৎসগুলির সাথে সেগুলি পুনরায় মিলিয়ে নিন।',
        'প্রমাণ উপস্থাপনার ক্ষেত্রে ভারসাম্য বজায় রাখার প্রয়োজন। কিছু যুক্তিকে শক্তিশালী করার জন্য প্রতিপক্ষের মতামত এবং প্রতিক্রিয়া যুক্ত করা উচিত।',
        'নিবন্ধে কিছু তথ্য পুরানো বলে মনে হচ্ছে। প্রাসঙ্গিকতা এবং সঠিকতার জন্য সর্বশেষ তথ্য এবং গবেষণা অন্তর্ভুক্ত করার কথা বিবেচনা করুন।',
        'অংশ বিশেষে অপ্রয়োজনীয় তথ্য যোগ করা হয়েছে যা মূল বিষয় থেকে মনোযোগ সরিয়ে দেয়। প্রাসঙ্গিক তথ্যের উপর গুরুত্বারোপ করুন।',
        'নিবন্ধের কিছু অংশ কপিরাইট নীতিমালার সাথে অসঙ্গতিপূর্ণ হতে পারে। অনুগ্রহ করে পুনর্বিবেচনা করে পাঠ্যগুলিকে পুনর্লিখন করুন।'
    ]
    },
    templates: {
        development: 'Wn/bn/উন্নয়ন চলছে',
        tasks: 'Wn/bn/করণীয়',
        controversial: 'Wn/bn/বিতর্কিত',
        styleIssue: 'Wn/bn/রচনাগত সমস্যা',
        publish: 'Wn/bn/প্রকাশিত',
        oldPublish: 'Wn/bn/প্রকাশ করুন',
        date: 'Wn/bn/তারিখ'
    },
    editSummaries: {
        published: 'নিরীক্ষণের পর নিবন্ধ প্রকাশিত হয়েছে',
        developmentNeeded: 'নিবন্ধকে পুনরায় উন্নয়নের জন্য চিহ্নিত করা হয়েছে'
    },
    reviewSection: {
        title: '== [সংস্করণ সংখ্যা: {revisionNumber}] এর নিরীক্ষণ - ফলাফল: {result} ==',
        template: `{{Wn/bn/প্রকাশন নিরীক্ষা
| সংখ্যা = {revisionNumber}
| তারিখ = {date}
| নিরীক্ষক = {reviewer}
| ফলাফল = {result}
| বার্তা  = {comments}
}}`,
        successResult: 'সফল',
        failResult: 'প্রস্তুত নয়'
    },
    reviewSummary: 'সংস্করণ সংখ্যা {revisionNumber}-এর নিরীক্ষণ',
    reviewTemplates: ['Wn/bn/নিরীক্ষা', 'Wn/bn/অনিরীক্ষিত প্রকাশ']
};

mw.loader.using(['@wikimedia/codex', 'mediawiki.api', 'mediawiki.util']).then(function(require) {
    const { createMwApp } = require('vue');
    const { CdxButton, CdxDialog, CdxTextInput, CdxProgressBar, CdxLabel, CdxSelect, CdxMessage, CdxRadio, CdxTextArea, CdxAccordion, CdxCheckbox } = require('@wikimedia/codex');

    const mountPoint = document.body.appendChild(document.createElement('div'));
    
    function convertToBengaliDigits(number) {
        const bengaliDigits = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'];
        return number.toString().replace(/\d/g, digit => bengaliDigits[digit]);
    }    

    function convertToBengaliDate(date, format = 'full') {
        const bengaliDigits = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'];
        const bengaliMonths = ['জানুয়ারি', 'ফেব্রুয়ারি', 'মার্চ', 'এপ্রিল', 'মে', 'জুন', 'জুলাই', 'আগস্ট', 'সেপ্টেম্বর', 'অক্টোবর', 'নভেম্বর', 'ডিসেম্বর'];
    
        const convertToBengaliDigits = (num) => num.toString().split('').map(digit => bengaliDigits[parseInt(digit)]).join('');
    
        const day = convertToBengaliDigits(date.getDate());
        const month = bengaliMonths[date.getMonth()];
        const year = convertToBengaliDigits(date.getFullYear());
    
        if (format === 'short') {
            return `${day} ${month}, ${year}`;
        } else {
            const hours = convertToBengaliDigits(date.getUTCHours().toString().padStart(2, '0'));
            const minutes = convertToBengaliDigits(date.getUTCMinutes().toString().padStart(2, '0'));
            return `${day} ${month} ${year}, ${hours}:${minutes} (আন্তর্জাতিক সময়)`;
        }
    }     

    const app = createMwApp({
        data() {
            return {
            	localization: localization,
                revision: convertToBengaliDigits(mw.config.get('wgRevisionId')),
                revisionEng: mw.config.get('wgRevisionId'),
                date: convertToBengaliDate(new Date()),
                reviewer: mw.config.get('wgUserName'),
                copyright: 'pass',
                neutrality: 'pass',
                style: 'pass',
                content: 'pass',
                comments: '',
                showDialog: false,
                showMessage: false,
                messageType: '',
                messageContent: '',
                showProgress: false,
                showConfirmDialog: false,
                showSuccessDialog: false,
                showSubmitProgress: false,
                articleCreator: '',
                articleCreatorTalk: '',
                articleCreationDate: '',
                totalEdits: 0,
                lastEditor: '',
                lastEditorTalk: '',
                showCommentsModal: false,
                positiveFeedback: localization.commentSuggestions.positiveFeedback,
                constructiveFeedback: localization.commentSuggestions.constructiveFeedback,
                tagAuthor: false,
                originalTag: '',
                userModified: false
            };
        },
        computed: {
            isReviewValid() {
                const hasFailedCriteria = ['copyright', 'neutrality', 'style', 'content'].some(
                    criteria => this[criteria] === 'fail'
                );
                return !hasFailedCriteria || (hasFailedCriteria && this.comments.trim() !== '');
            },
            defaultTag() {
                return this.articleCreator ? `@[[User:${this.articleCreator}|${this.articleCreator}]] ` : '';
            },
            filteredCommentSuggestions() {
                const allPassed = ['copyright', 'neutrality', 'style', 'content'].every(
                    criteria => this[criteria] === 'pass'
                );
                return allPassed ? this.positiveFeedback : this.constructiveFeedback;
            }
        },        
        watch: {
            tagAuthor(newValue) {
                if (newValue) {
                    this.addTag();
                } else {
                    this.removeTag();
                }
            },
            articleCreator() {
                this.originalTag = this.defaultTag;
                if (this.tagAuthor) {
                    this.addTag();
                }
            }
        },
        methods: {
            openReviewDialog() {
                this.showDialog = true;
                this.fetchArticleInfo();
            },
            fetchArticleInfo() {
                wikiNirikshan.api.get({
                    action: 'query',
                    prop: 'revisions|info',
                    rvprop: 'timestamp|user',
                    rvlimit: 'max',
                    rvdir: 'newer',
                    titles: mw.config.get('wgPageName'),
                    formatversion: '2'
                }).then(data => {
                    const page = data.query.pages[0];
                    const revisions = page.revisions;
            
                    // Set article creator, creation date, total edits, and last editor
                    this.articleCreator = revisions[0].user;
                    this.articleCreatorTalk = `User talk:${revisions[0].user}`;
                    this.articleCreationDate = convertToBengaliDate(new Date(revisions[0].timestamp), 'short');
                    this.totalEdits = convertToBengaliDigits(revisions.length);
                    this.lastEditor = revisions[revisions.length - 1].user;
                    this.lastEditorTalk = `User talk:${revisions[revisions.length - 1].user}`;
            
                }).catch(error => {
                    console.error('Error fetching article info:', error);
            
                });
            },   
            submitReview() {
                if (!this.isReviewValid) {
                    this.displayMessage('warning', localization.warningMessage);
                    return;
                }
                this.showConfirmDialog = true;
            },
            confirmSubmitReview() {
                this.showConfirmDialog = false;
                this.showSubmitProgress = true;

                this.updateArticle()
                    .then(() => this.updateTalkPage())
                    .then(() => {
                        this.showSubmitProgress = false;
                        this.showDialog = false;
                        this.showSuccessDialog = true;
                        this.displayMessage('success', localization.successDialogMessage);
                    })
                    .catch(error => {
                        console.error('Error during review process:', error);
                        this.showSubmitProgress = false;
                        this.displayMessage('error', localization.errorDialogMessage);
                    });
            },
            updateArticle() {
                return wikiNirikshan.api.get({
                    action: 'query',
                    prop: 'revisions',
                    rvprop: 'content',
                    rvslots: '*',
                    titles: mw.config.get('wgPageName'),
                    formatversion: '2'
                }).then(data => {
                    const page = data.query.pages[0];
                    let content = page.revisions[0].slots.main.content;
            
                    // Remove any existing review templates
                    const reviewTemplates = this.localization.reviewTemplates || [
                        'Wn/bn/নিরীক্ষা', 
                        'Wn/bn/অনিরীক্ষিত প্রকাশ'
                    ];
                    const reviewRegExp = new RegExp(`\\{\\{(${reviewTemplates.join('|')})\\}\\}`, 'gi');
                    content = content.replace(reviewRegExp, '');
            
                    // Update content based on review outcome
                    if (this.isReviewSuccessful()) {
                        content = this.updateContentForSuccessfulReview(content);
                    } else {
                        content = this.updateContentForFailedReview(content);
                    }
            
                    // Post the updated content with the appropriate summary
                    return wikiNirikshan.api.postWithToken('csrf', {
                        action: 'edit',
                        title: mw.config.get('wgPageName'),
                        text: content,
                        summary: this.isReviewSuccessful() 
                            ? this.localization.editSummaries.published 
                            : this.localization.editSummaries.developmentNeeded,
                        bot: false,
                        nocreate: true
                    });
                });
            },  
            updateContentForSuccessfulReview(content) {
            	// Remove development, controversial, and style issue templates
                const templateRegex = new RegExp(`\\{\\{(${this.localization.templates.development}|${this.localization.templates.controversial}|${this.localization.templates.styleIssue}|${this.localization.templates.tasks})\\}\\}\\s*`, 'gi');

                // Trim any extra whitespace at the end of the content
                content = content.trim();

                // Find the position of the first category
                const categoryIndex = content.search(/\[\[Category:/i);

                // Check and handle publish templates
				const oldPublishPattern = `{{${this.localization.templates.oldPublish}}}`;
				const publishPattern = `{{${this.localization.templates.publish}}}`;
				
				if (content.includes(oldPublishPattern)) {
				    // Replace oldPublish with publish
				    content = content.replace(oldPublishPattern, publishPattern);
				} else if (!content.includes(publishPattern)) {
				    // Add publish template if neither oldPublish nor publish exists
				    const publishTemplate = `\n${publishPattern}`;
				    if (categoryIndex !== -1) {
				        // Insert publish template before categories
				        content = content.slice(0, categoryIndex) + publishTemplate + '\n' + content.slice(categoryIndex);
				    } else {
				        // If no categories, add publish template at the end
				        content += publishTemplate;
				    }
				}

                // Add the date template if it doesn't exist
                if (!content.includes(`{{${this.localization.templates.date}|`)) {
                    content = `{{${this.localization.templates.date}|{{subst:#time:F j, Y}}}}\n${content}`;
                }

                return content;
            },
            updateContentForFailedReview(content) {
			    // Define the mapping of criteria to Bengali labels
			    const criteriaLabels = {
			        'copyright': 'কপিরাইট',
			        'neutrality': 'নিরপেক্ষতা',
			        'style': 'শৈলী',
			        'content': 'বিষয়বস্তু'
			    };
			    // Collect failed criteria
			    const failedCriteria = ['copyright', 'neutrality', 'style', 'content']
			        .filter(criteria => this[criteria] === 'fail')
			        .map(criteria => criteriaLabels[criteria]);
			
			    // Add the tasks template with failed criteria as parameters
			    if (failedCriteria.length > 0) {
			        const tasksTemplate = `{{${this.localization.templates.tasks}|${failedCriteria.join('|')}}}\n`;
			        content = tasksTemplate + content;
			    }
			
			    // Remove both publish and oldPublish templates 
			    content = content.replace( 
			        new RegExp(`\\{\\{${this.localization.templates.publish}\\}\\}`, 'gi'),  
			        '' 
			    ).replace( 
			        new RegExp(`\\{\\{${this.localization.templates.oldPublish}\\}\\}`, 'gi'),  
			        '' 
			    ); 
			 
			    // Clean up any double newlines that might be left after template removal 
			    content = content.replace(/\n\s*\n\s*\n/g, '\n\n'); 
			 
			    return content.trim(); 
			},
            updateTalkPage() {
                const talkPageTitle = `Talk:${mw.config.get('wgTitle')}`;
                const reviewResult = this.isReviewSuccessful() ? localization.reviewSection.successResult : localization.reviewSection.failResult;
                const bengaliDate = convertToBengaliDate(new Date());
                
                const reviewTemplate = localization.reviewSection.title
                    .replace('{revisionNumber}', convertToBengaliDigits(this.revision))
                    .replace('{result}', reviewResult) +
                    '\n' + localization.reviewSection.template
                    .replace('{revisionNumber}', convertToBengaliDigits(this.revision))
                    .replace('{date}', bengaliDate)
                    .replace('{reviewer}', this.reviewer)
                    .replace('{result}', reviewResult)
                    .replace('{comments}', this.comments);
            
                return wikiNirikshan.api.get({
                    action: 'query',
                    prop: 'revisions',
                    rvprop: 'content',
                    rvslots: '*',
                    titles: talkPageTitle,
                    formatversion: '2'
                }).then(data => {
                    const page = data.query.pages[0];
                    let content = page.revisions ? page.revisions[0].slots.main.content : '';
            
                    content = content.trim() + (content ? '\n\n' : '') + reviewTemplate;
            
                    return wikiNirikshan.api.postWithToken('csrf', {
                        action: 'edit',
                        title: talkPageTitle,
                        text: content,
                        summary: localization.reviewSummary.replace('{revisionNumber}', this.revision),
                        bot: false,
                        createonly: false
                    });
                });
            },            
            isReviewSuccessful() {
                return ['copyright', 'neutrality', 'style', 'content'].every(
                    criteria => this[criteria] === 'pass'
                );
            },
            displayMessage(type, content) {
                this.messageType = type;
                this.messageContent = content;
                this.showMessage = true;
                setTimeout(() => {
                    this.showMessage = false;
                }, 3000);
            },
            handleSuccessDialogClose() {
                this.showSuccessDialog = false;
                window.location.reload();
            },
            handleConfirmDialogCancel() {
                this.showConfirmDialog = false;
            },
            cancelReview() {
                this.showDialog = false;
            },
            openCommentsModal() {
                this.showCommentsModal = true;
            },
            closeCommentsModal() {
                this.showCommentsModal = false;
            },
            copyComment(comment) {
                this.comments += comment + '\n';  
                this.closeCommentsModal();
            },
            addTag() {
                if (!this.comments.startsWith(this.originalTag)) {
                    this.comments = this.originalTag + this.comments;
                }
            },
            removeTag() {
                if (this.comments.startsWith(this.originalTag) && !this.userModified) {
                    this.comments = this.comments.replace(this.originalTag, '');
                }
            },
            handleCommentInput(event) {
                const newValue = event.target.value;
                this.comments = newValue;

                if (this.tagAuthor && !newValue.startsWith(this.originalTag)) {
                    this.userModified = true;
                    this.tagAuthor = false;
                } else if (newValue.startsWith(this.originalTag)) {
                    this.userModified = false; 
                }
            },
            openHelpPage() {
                window.open('https://incubator.wikimedia.org/wiki/Wn/bn/%E0%A6%89%E0%A6%87%E0%A6%95%E0%A6%BF%E0%A6%B8%E0%A6%82%E0%A6%AC%E0%A6%BE%E0%A6%A6:%E0%A6%A8%E0%A6%BF%E0%A6%AC%E0%A6%A8%E0%A7%8D%E0%A6%A7_%E0%A6%A8%E0%A6%BF%E0%A6%B0%E0%A7%80%E0%A6%95%E0%A7%8D%E0%A6%B7%E0%A6%A3%E0%A7%87%E0%A6%B0_%E0%A6%AA%E0%A6%A6%E0%A7%8D%E0%A6%A7%E0%A6%A4%E0%A6%BF', '_blank');
            },
            openPlagiarismCheckPage() {
                window.open(`https://copyvios.toolforge.org/?lang=incubator%3A%3Aincubatorwiki&project=wikimedia&oldid=${this.revisionEng}&action=search&use_engine=1&use_links=1&turnitin=1&use_engine=0`, '_blank');
               }
        },
        created() {
            this.originalTag = this.defaultTag;
        },
        template: `
    <div>
    <cdx-dialog v-if="showDialog" :open="showDialog" @close="cancelReview" @update:open="showDialog = $event" :title="localization.dialogTitle" :close-button-label="localization.closeButtonLabel">
        <template #default>
            <div style="margin-bottom: 15px;">
                <cdx-accordion>
                    <template #title>
                        {{ localization.articleInfoTitle }}
                    </template>
                    <div style="font-size: 14px;">
                        <p>{{ localization.creatorLabel }} 
                            <a :href="'https://incubator.wikimedia.org/wiki/User:' + articleCreator" target="_blank">{{ articleCreator }}</a> 
                            (<a :href="'https://incubator.wikimedia.org/wiki/' + articleCreatorTalk" target="_blank">{{ localization.talkLinkText }}</a>)
                        </p>
                        <p>{{ localization.creationDateLabel }} {{ articleCreationDate }}</p>
                        <p>{{ localization.totalEditsLabel }} {{ totalEdits }}</p>
                        <p>{{ localization.lastEditorLabel }} 
                            <a :href="'https://incubator.wikimedia.org/wiki/User:' + lastEditor" target="_blank">{{ lastEditor }}</a>
                            (<a :href="'https://incubator.wikimedia.org/wiki/' + lastEditorTalk" target="_blank">{{ localization.talkLinkText }}</a>)
                        </p>
                    </div>
                </cdx-accordion>
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-label>{{ localization.plagiarismCheckLabel }}</cdx-label>
                <div style="margin-bottom: 10px;">
                    <cdx-button action="normal" @click="openPlagiarismCheckPage">
                        {{ localization.plagiarismCheckLinkText }}
                    </cdx-button>
                </div>
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-label input-id="revision">{{ localization.revisionNumberLabel }}</cdx-label>
                <cdx-text-input id="revision" v-model="revision" readonly />
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-label input-id="date">{{ localization.dateLabel }}</cdx-label>
                <cdx-text-input id="date" v-model="date" readonly />
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-label input-id="reviewer">{{ localization.reviewerLabel }}</cdx-label>
                <cdx-text-input id="reviewer" v-model="reviewer" readonly />
            </div>

            <div v-for="criteria in ['copyright', 'neutrality', 'style', 'content']" :key="criteria" style="margin-bottom: 15px; margin-top: 5px;">
                <cdx-field :is-fieldset="true" :hide-label="false" style="display: flex; align-items: center;">
                    <cdx-label :input-id="criteria" style="min-width: 110px; margin-right: 10px;">{{ localization.criteriaLabels[criteria] }}</cdx-label>
                    <div style="display: flex;">
                        <cdx-radio v-model="this[criteria]" :name="criteria + '-radios'" input-value="pass" :inline="true" style="margin-right: 10px;">
                            {{ localization.passLabel }}
                        </cdx-radio>
                        <cdx-radio v-model="this[criteria]" :name="criteria + '-radios'" input-value="fail" :inline="true">
                            {{ localization.failLabel }}
                        </cdx-radio>
                    </div>
                </cdx-field>
            </div>

            <div style="margin-bottom: 15px; margin-top: 20px; border-top: 1px solid #d2d5d9; padding-top: 15px;">
                <cdx-button @click="openCommentsModal">{{ localization.showSuggestedCommentsButton }}</cdx-button>

                <cdx-dialog v-if="showCommentsModal" :open="showCommentsModal" @close="closeCommentsModal" :title="localization.suggestedCommentsTitle">
                    <template #default>
                        <ul>
                            <li v-for="(comment, index) in filteredCommentSuggestions" :key="index" style="margin-bottom: 10px;">
                                <div style="display: flex; justify-content: space-between; align-items: center;">
                                    <span>{{ comment }}</span>
                                    <span @click="copyComment(comment)" style="cursor: pointer; color: #3366CC; margin-left: 10px;">
                                        {{ localization.copyText }}
                                    </span>
                                </div>
                            </li>
                        </ul>
                    </template>
                    <template #footer>
                        <cdx-button action="progressive" @click="closeCommentsModal">
                            {{ localization.closeButtonLabel }}
                        </cdx-button>
                    </template>
                </cdx-dialog>
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-label input-id="comments">{{ localization.commentsLabel }}</cdx-label>
                <cdx-textarea id="comments" v-model="comments" rows="4" @input="handleCommentInput" />
            </div>

            <div style="margin-bottom: 15px;">
                <cdx-checkbox v-model="tagAuthor">
                    {{ localization.tagAuthorLabel }}
                </cdx-checkbox>
            </div>
        </template>
        <template #footer>
            <div style="display: flex; justify-content: space-between; align-items: center;">
                <div>
                    <cdx-button action="progressive" weight="primary" @click="submitReview" style="margin-right: 10px;">
                        {{ localization.submitReviewButton }}
                    </cdx-button>
                    <cdx-button action="normal" @click="cancelReview">
                        {{ localization.cancelButton }}
                    </cdx-button>
                </div>
                <div>
                    <cdx-button action="normal" @click="openHelpPage">
                        {{ localization.helpButton }}
                    </cdx-button>
                </div>
            </div>
            <cdx-progress-bar v-if="showSubmitProgress" inline style="margin-top: 10px;" />
        </template>
    </cdx-dialog>

    <cdx-dialog v-if="showConfirmDialog" :open="showConfirmDialog" @close="handleConfirmDialogCancel" @update:open="showConfirmDialog = $event" :title="localization.confirmDialogTitle">
        <template #default>
            <p>{{ localization.confirmDialogMessage }}</p>
            <p>{{ localization.reviewResultLabel }} <strong>{{ isReviewSuccessful() ? localization.reviewSection.successResult : localization.reviewSection.failResult }}</strong></p>
        </template>
        <template #footer>
            <cdx-button action="progressive" weight="primary" @click="confirmSubmitReview" style="margin-right: 10px;">
                {{ localization.confirmSubmitButton }}
            </cdx-button>
            <cdx-button action="normal" @click="handleConfirmDialogCancel">
                {{ localization.cancelButton }}
            </cdx-button>
        </template>
    </cdx-dialog>

    <cdx-dialog v-if="showSuccessDialog" :open="showSuccessDialog" @close="handleSuccessDialogClose" @update:open="showSuccessDialog = $event" :title="localization.successDialogTitle">
        <template #default>
            <p>{{ localization.successDialogMessage }}</p>
        </template>
        <template #footer>
            <cdx-button action="progressive" weight="primary" @click="handleSuccessDialogClose">
                {{ localization.okButton }}
            </cdx-button>
        </template>
    </cdx-dialog>

    <cdx-progress-bar v-if="showProgress" inline />

    <div v-if="showMessage" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); width: 60%; z-index: 999;">
        <cdx-message :type="messageType" :fade-in="true" :auto-dismiss="true" :display-time="3000" dismiss-button-label="Close">
            {{ messageContent }}
        </cdx-message>
    </div>
</div>
        `
    })
    .component('cdx-button', CdxButton)
    .component('cdx-dialog', CdxDialog)
    .component('cdx-text-input', CdxTextInput)
    .component('cdx-textarea', CdxTextArea)
    .component('cdx-radio', CdxRadio)
    .component('cdx-progress-bar', CdxProgressBar)
    .component('cdx-label', CdxLabel)
    .component('cdx-message', CdxMessage)
    .component('cdx-accordion', CdxAccordion)
    .component('cdx-checkbox', CdxCheckbox)
    .mount(mountPoint);

    if (mw.config.get('wgNamespaceNumber') === 0 && document.getElementById('review')) {
    var skin = mw.config.get('skin');
    var portlet = skin === 'vector-2022' ? "p-cactions" : "p-tb";
    
    const dialogTrigger = mw.util.addPortletLink(portlet, '#', localization.reviewPortletLink, 'ca-review', localization.reviewPortletTitle);
    
    dialogTrigger.addEventListener('click', () => {
        app.openReviewDialog();
    });
}
});
// </nowiki>