Jump to content

User:Asked42/LeadManager.js

From Wikinews, the free news source you can write!

Note: After saving, you may have to bypass your browser's cache to see the changes. Mozilla / Firefox / Safari: hold down Shift while clicking Reload, or press Ctrl-Shift-R (Cmd-Shift-R on Apple Mac); IE: hold Ctrl while clicking Refresh, or press Ctrl-F5; Konqueror: simply click the Reload button, or press F5; Opera users may need to completely clear their cache in Tools→Preferences. — More skins

// <nowiki>

if (typeof leadManager === 'undefined') {
    var leadManager = {};
}
leadManager.api = new mw.Api();
mw.loader.load('//en.wikinews.org/w/index.php?title=User:Bawolff/sanbox/leadGenerator.js&action=raw&ctype=text/javascript');

leadManager.config = {
    buttonMinWidth: '140px',
    categoryName: 'Published',
    defaultImageSize: '150x150px (100x100px)',
    updateDelay: 100,
    messageDisplayTime: 3000,
    leadPages: [
        "Template:Lead_article_1",
        "Template:Lead_article_2",
        "Template:Lead_article_3",
        "Template:Lead_article_4",
        "Template:Lead_article_5"
    ],
};

leadManager.i18n = {
    // Form labels
    title: 'Title',
    sentences: 'Number of Sentences',
    image: 'Image',
    summary: 'Summary',
    imageSize: 'Image Size',
    newsType: 'Type of News',
    shortTitle: 'Short Title',
    leadTemplate: 'Lead Article Template',

    // Buttons
    autoFetch: 'Fetch Automatically',
    startUpdate: 'Start Update!',
    reset: 'Start Fresh',
    confirm: 'Yes, Confirm.',
    cancel: 'Cancel',
    ok: 'Okay',

    // Messages
    enterTitle: 'Please provide a title or use keywords like "ML1", "ML2"!',
    enterSummary: 'Please provide a summary.',
    searching: 'Searching for data...',
    pageNotFound: 'Page not found.',
    fetchSuccess: 'Data fetched successfully.',
    fetchDescriptionSuccess: 'Description fetched successfully.',
    fetchError: 'Error occurred while fetching data: ',
    updateSuccess: 'Lead articles updated successfully.',
    updateError: 'An error occurred while updating the lead articles.',

    // Placeholders
    titlePlaceholder: 'Enter title',
    imagePlaceholder: 'Without "File:" namespace',
    summaryPlaceholder: 'Write a short description...',
    shortTitlePlaceholder: 'Short title (optional)',

    // Dialog titles
    updateConfirmTitle: 'Update Confirmation',
    updateSuccessTitle: 'Successful Update',

    // Other texts
    additionalOptions: 'Additional Options',
    optionsDescription: 'Options for image size, template, and updates',
    singleUpdateLabel: 'Single Update',
    singleUpdateDesc: 'Selecting this option will update only the selected template.',
    confirmUpdateMessage: 'Are you sure you want to update the lead articles?',
    explicitUpdateLabel: 'Explicit Update',
    explicitUpdateDesc: 'Specify custom template shuffle pattern',
    templateShufflePreview: 'Template Shuffle Preview:',
    
    explicitUpdateFormat: 'Use "template->template" separated by commas',
	explicitUpdateExamples: 'Example moves:',
	explicitUpdateExample1: '• Single move: "1->2" ',
	explicitUpdateExample2: '• Multiple moves: "1->2, 2->3" ',
	explicitUpdateExample3: '• Complex pattern: "1->3, 3->2, 2->4" ',
	imageSizeFormat: '"Size for selected template: 150x150px (others: 100x100px)',

};

leadManager.imageMap = { 
	
	"United States": "Flag of the United States.svg",
    "United Kingdom": "Flag of the United Kingdom (3-5).svg",
    "Canada": "Flag of Canada.svg",
    "Australia": "Flag of Australia.svg",
    "India": "Flag of India.svg",
    "Bangladesh": "Flag of Bangladesh.svg",
    "Pakistan": "Flag of Pakistan.svg",
    "Sri Lanka": "Flag of Sri Lanka.svg",
    "Nepal": "Flag of Nepal.svg",
    "Bhutan": "Flag of Bhutan.svg",
    "Maldives": "Flag of Maldives.svg",
    "China": "Flag of the People's Republic of China.svg",
    "Japan": "Flag of Japan.svg",
    "Russia": "Flag of Russia.svg",
    "Brazil": "Flag of Brazil.svg",
    "Germany": "Flag of Germany.svg",
    "France": "Flag of France.svg",
    "Italy": "Flag of Italy.svg",
    "Spain": "Flag of Spain.svg",
    "Saudi Arabia": "Flag of Saudi Arabia.svg",
    "Iran": "Flag of Iran.svg",
    "Iraq": "Flag of Iraq.svg",
    "Israel": "Flag of Israel.svg",
    "Indonesia": "Flag of Indonesia.svg",
    "Malaysia": "Flag of Malaysia.svg",
    "Thailand": "Flag of Thailand.svg",
    "South Korea": "Flag of South Korea.svg",
    "North Korea": "Flag of North Korea.svg",
    "South Africa": "Flag of South Africa.svg",
    "Egypt": "Flag of Egypt.svg",
    "Turkey": "Flag of Turkey.svg",
    "Mexico": "Flag of Mexico.svg",
    "Argentina": "Flag of Argentina.svg",
    "Nigeria": "Flag of Nigeria.svg",
    "Kenya": "Flag of Kenya.svg",
    "Vietnam": "Flag of Vietnam.svg",
    "Singapore": "Flag of Singapore.svg",
    "Philippines": "Flag of Philippines.svg",
    "Greece": "Flag of Greece.svg",
    "Netherlands": "Flag of Netherlands.svg",
    "Switzerland": "Flag of Switzerland.svg",
    "Poland": "Flag of Poland.svg",
    "Ukraine": "Flag of Ukraine.svg",
    "Sweden": "Flag of Sweden.svg",
    "Norway": "Flag of Norway.svg",
    "Finland": "Flag of Finland.svg",
    "Portugal": "Flag of Portugal.svg",
    "Czech Republic": "Flag of Czech Republic.svg",
    "Myanmar": "Flag of Myanmar.svg",
    "Qatar": "Flag of Qatar.svg",
    "United Arab Emirates": "Flag of the United Arab Emirates.svg",
    "Kuwait": "Flag of Kuwait.svg",
    "Denmark": "Flag of Denmark.svg",
    "Belgium": "Flag of Belgium.svg",
    "Ireland": "Flag of Ireland.svg",
    "Austria": "Flag of Austria.svg",
    "Bulgaria": "Flag of Bulgaria.svg",
    "Romania": "Flag of Romania.svg",
    "New Zealand": "Flag of New Zealand.svg",
    "Jamaica": "Flag of Jamaica.svg",
    "Belize": "Flag of Belize.svg",
    "Seychelles": "Flag of Seychelles.svg",
    "Fiji": "Flag of Fiji.svg",
    "Papua New Guinea": "Flag of Papua New Guinea.svg",
    "Samoa": "Flag of Samoa.svg",
    "Vanuatu": "Flag of Vanuatu.svg",
    
    "Crime and Law": "Scale of justice 2.svg",
    "Culture and entertainment": "Munken kino (kinolerret).jpg",
    "Disasters and accidents": "Blue lightbar (fire and rescue).jpg",
    "Economy and business": "Exchange Money Conversion to Foreign Currency.jpg",
    "Education": "Curiosity at peak.jpg",
    "Environment": "Hopetoun falls.jpg",
    "Health": "Star of life2.svg",
    "Obituaries": "Wikinews-logo-obituaries.png",
    "Politics and conflicts": "RIAN archive 828797 Mikhail Gorbachev addressing UN General Assembly session.jpg",
    "Politics": "Election MG 3455.JPG",
    "Elections": "Election MG 3455.JPG",
    "Disasters": "Amazing-natural-disasters.jpg",
    "Natural disasters": "Amazing-natural-disasters.jpg",
    "Accidents": "Post-and-Grant-Avenue-Look.jpg",
    "Earthquakes": "Valdivia after earthquake, 1960.jpg",
    "Science": "Stylised atom with three Bohr model orbits and stylised nucleus (encircled).svg",
    "Technology": "Technologie.jpg",
    "Science and technology": "Wikinoticias Ciencia y Tecnología.svg",
    "Sports": "Youth-soccer-indiana.jpg",
    "Cricket": "Muralitharan bowling to Adam Gilchrist.jpg"

};

leadManager.helpers = {
    extractFirstImage: function(content) {
        const imageRegex = /\[\[(File|Image):([^\]\|]+)(?:\|[^\]]*)?\]\]/i;
        const match = content.match(imageRegex);
        return match ? match[2].trim() : null;
    },

    extractInfoboxes: function(content) {
        if (!content) return [];
        
        const infoboxes = [];
        const templateRegex = /\{\{([^{}|\n]+)(?:\|[\s\S]*?)?\}\}/g;
        let match;
        
        while ((match = templateRegex.exec(content)) !== null) {
            let templateName = match[1].trim();
            templateName = templateName.replace(/^Template:/, '');
            infoboxes.push(templateName);
        }
        
        return infoboxes;
    },

    extractCategories: function(content) {
        if (!content) return [];
        
        const categories = [];
        const categoryRegex = /\[\[Category:([^\]\|]+)(?:\|[^\]]*)?\]\]/g;
        let match;
        while ((match = categoryRegex.exec(content)) !== null) {
            categories.push(match[1]);
        }
        return categories;
    },
    findBackupImage: function(content, pageTitle) {
	    // First check infoboxes
	    const infoboxes = this.extractInfoboxes(content);
	    for (const infobox of infoboxes) {
	        const lowerInbox = infobox.toLowerCase();
	        
	        if (leadManager.imageMap[lowerInbox] || 
	            Object.keys(leadManager.imageMap).find(key => key.toLowerCase() === lowerInbox)) {
	            return leadManager.imageMap[Object.keys(leadManager.imageMap)
	                .find(key => key.toLowerCase() === lowerInbox) || infobox];
	        }
	        
	        if (Bawolff?.leadGen?.imgMap?.[lowerInbox] || 
	            Object.keys(Bawolff?.leadGen?.imgMap || {}).find(key => key.toLowerCase() === lowerInbox)) {
	            return Bawolff.leadGen.imgMap[Object.keys(Bawolff.leadGen.imgMap)
	                .find(key => key.toLowerCase() === lowerInbox) || infobox];
	        }
	    }
	
	    // Then check categories
	    const categories = this.extractCategories(content);
	    for (const category of categories) {
	        const lowerCategory = category.toLowerCase();
	       
	        if (leadManager.imageMap[lowerCategory] || 
	            Object.keys(leadManager.imageMap).find(key => key.toLowerCase() === lowerCategory)) {
	            return leadManager.imageMap[Object.keys(leadManager.imageMap)
	                .find(key => key.toLowerCase() === lowerCategory) || category];
	        }
	        
	        if (Bawolff?.leadGen?.imgMap?.[lowerCategory] || 
	            Object.keys(Bawolff?.leadGen?.imgMap || {}).find(key => key.toLowerCase() === lowerCategory)) {
	            return Bawolff.leadGen.imgMap[Object.keys(Bawolff.leadGen.imgMap)
	                .find(key => key.toLowerCase() === lowerCategory) || category];
	        }
	    }
	
	    return '';
	},
    detectType: function(content) {
	    if (!content) return 'none';
	    
	    if (content.match(/\{\{[bB]reaking(?: [nN]ews)?\}\}/)) {
	        return 'breaking-news';
	    } else if (content.match(/\{\{[iI]nterview(?:\|[^}]*)?\}\}/i)) {
	        return 'exclusive-interview';
	    } else if (content.match(/\{\{[oO]riginal(?: reporting)?(?:\|[^}]*)?\}\}/i)) {
	        return 'original-report';
	    } else if (content.match(/\{\{[sS]pecial(?: [rR]eport)?(?:\|[^}]*)?\}\}/i)) {
	        return 'special-report';
	    }
	    
	    return 'none';
	},
       
    cleanupContent: function(content, sentenceCount = 2) {
	    if (!content) return '';
		
		    let cleanText = content;
		
		    const removeComments = (text) => text.replace(/<!--[\s\S]*?-->/g, '');
		    const removeNoticeTemplates = (text) => text.replace(
		        /\{\{(?:Ombox|[Aa]mbox|[Mm]box|[Nn]otice|[Mm]essage)[^}]*?\|\s*text\s*=\s*([^}\|]+)(?:[^}]*?)\}\}/g,
		        '$1'
		    );
		    const fixWikilinks = (text) => text.replace(
		        /\{\{[Ww]\|((?:[^|{}]|\{\{[^{}]*\}\})*?)(?:\|((?:[^|{}]|\{\{[^{}]*\}\})*?))?\}\}/g,
		        (match, p1, p2) => (p2 ? `[[${p2}]]` : `[[${p1}]]`)
		    );
		    const removeMediaLinks = (text) =>
		        text.replace(/\[\[(?:File|Image|Media):[^\[\]]*?\]\]/gi, '')
		            .replace(/\[\[(?:File|Image|Media):(?:[^\[\]]|\[\[(?:[^\[\]]|\[\[[^\[\]]*\]\])*\]\])*\]\]/gi, '');
		    const removeTemplates = (text) => {
		        const stripTemplates = /\{\{[^\}\{]*(?:\{\{[^\}\{]*(?:\{\{[^\}\{]*(?:\{\{[^\}\{]*\}\})?\}\})?\}\})?\}\}/g;
		        while (text.match(/\{\{[^\}]*\}\}/)) {
		            text = text.replace(stripTemplates, '');
		        }
		        return text;
		    };
		    const removeCategories = (text) => text.replace(/\[\[(?:Category):[\s\S]*?\]\]/gi, '');
		    const cleanFormatting = (text) =>
		        text.replace(/'''''(.+?)'''''/g, '$1')
		            .replace(/'''(.+?)'''/g, '$1')
		            .replace(/''(.+?)''/g, '$1')
		            .replace(/[«»‹›『』「」]/g, '');
		    const handleLinks = (text) => {
		        const pipedLink = /\[\[(?:[^|\]]*\|)?([^\]]+)\]\]/g;
		        while (text.match(/\[\[/)) {
		            text = text.replace(pipedLink, '$1');
		        }
		        return text.replace(/\[(?:https?|ftp|gopher|irc):\/\/[^\]\s]*(?:\s+([^\]]+))?\]/g, '$1 ');
		    };
		    const removeHTMLandTables = (text) =>
		        text.replace(/<ref[^>]*?>[\s\S]*?<\/ref>/g, '')
		            .replace(/<ref[^>\/]*?\/>/g, '')
		            .replace(/<[^>]+>/g, '')
		            .replace(/\{\|[\s\S]*?\|\}/g, '');
		    const cleanWhitespace = (text) =>
		        text.replace(/(\r\n|\n|\r)/gm, ' ')
		            .replace(/&nbsp;/g, ' ')
		            .replace(/&[a-z]+;/g, '')
		            .replace(/\s+/g, ' ')
		            .trim();
		    const removeHeaders = (text) => text.replace(/==+\s*[^=]+\s*==+/g, '');
		
		    cleanText = removeComments(cleanText);
		    cleanText = removeNoticeTemplates(cleanText);
		    cleanText = fixWikilinks(cleanText);
		    cleanText = removeMediaLinks(cleanText);
		    cleanText = removeTemplates(cleanText);
		    cleanText = removeCategories(cleanText);
		    cleanText = cleanFormatting(cleanText);
		    cleanText = handleLinks(cleanText);
		    cleanText = removeHTMLandTables(cleanText);
		    cleanText = cleanWhitespace(cleanText);
		    cleanText = removeHeaders(cleanText);
	
	    const splitIntoSentences = (text) => {
	        
	        const exceptions = [
	            'Mr', 'Mrs', 'Ms', 'Dr', 'Prof', 'Sr', 'Jr', 'vs', 'etc',
	            'Inc', 'Ltd', 'Co', 'Corp', 'Ref', 'Ed', 'U.S', 'i.e', 'e.g', 'St'
	        ];
	        
	        const abbrPattern = exceptions.map(abbr => 
	            abbr.split('.').join('\\.?')
	        ).join('|');
	
	        let processedText = text;
	        
	        processedText = processedText.replace(
	            new RegExp(`"([^"]*?(${abbrPattern})\\.\\s+[^"]*?)"`, 'g'),
	            (match, p1, p2) => match.replace('. ', '.@@@')
	        );
	
	        processedText = processedText.replace(
	            new RegExp(`\\b(${abbrPattern})\\.\\s+(?=[A-Z][a-z]+|[0-9])`, 'g'),
	            '$1.@@@'
	        );
	        
	        processedText = processedText.replace(
	            new RegExp(`\\b(${abbrPattern})\\.\\s+(?=[A-Z]\\.)`, 'g'),
	            '$1.@@@'
	        );
	        
	        const sentences = processedText
	            
	            .split(/(?<=[.!?])(?<!\.@@@)\s+(?=[A-Z])/g)
	          
	            .map(sentence => sentence.replace(/@@@/g, ' '))
	            .filter(s => s.length > 0);
	
	        return sentences;
	    };
	
	    const sentences = splitIntoSentences(cleanText)
	        .filter(s => s.length > 0 && s.length < 500)
	        .slice(0, sentenceCount);
	
	    return sentences.join(' ');
	    }
	};

mw.loader.using('@wikimedia/codex').then(function(require) {
    const Vue = require('vue');
    const Codex = require('@wikimedia/codex');

    const mountPoint = document.body.appendChild(document.createElement('div'));

    Vue.createMwApp({
        data() {
            return {
                i18n: leadManager.i18n, 
                config: leadManager.config,
                leadImage: '',
                leadWidth: leadManager.config.defaultImageSize,
                leadType: 'none',
                leadTitle: '',
                leadShortTitle: '',
                leadSummary: '',
                startPosition: 1,
                sentenceCount: 2,
                singleUpdate: false,
                showMessage: false,
                messageType: '',
                messageContent: '',
                showProgress: false,
                showConfirmDialog: false,
                showSuccessDialog: false,
                isLoading: false,
                typeOptions: [
                    { label: 'None', value: 'none' },
                    { label: 'Breaking News', value: 'breaking-news' },
                    { label: 'Special Report', value: 'special-report' },
                    { label: 'Original Report', value: 'original-report' },
                    { label: 'Exclusive Interview', value: 'exclusive-interview' }
                ],
                sentenceOptions: [
                    { label: '1 Sentence', value: 1 },
                    { label: '2 Sentences', value: 2 },
                    { label: '4 Sentences', value: 4 },
                    { label: '6 Sentences', value: 6 }
                ],
                positionOptions: [
                    { label: 'Lead Article 1 (Row 1, top)', value: 1 },
                    { label: 'Lead Article 2 (Row 2, left)', value: 2 },
                    { label: 'Lead Article 3 (Row 2, right)', value: 3 },
                    { label: 'Lead Article 4 (Row 3, left)', value: 4 },
                    { label: 'Lead Article 5 (Row 3, right)', value: 5 }
                ],
                explicitUpdate: false,
		        moveCommandError: '',
            };
        },
        computed: {
            isFormValid: function() {
                return this.leadTitle && this.leadSummary;
            },
            i18n() {
                return leadManager.i18n;
            },
            config() {
                return leadManager.config;
            },
            moveCommandPlaceholder: function() {
		        const pos = this.startPosition;
		        switch(pos) {
		            case 1:
		                return "1->2, 2->3, 3->4, 4->5";
		            case 2:
		                return "2->3, 3->4, 4->5";
		            case 3:
		                return "3->4, 4->5";
		            case 4:
		                return "4->5";
		            case 5:
		                return "5->1";
		            default:
		                return "Example: 1->2, 2->3";
		        }
		    }
        },
        methods: {
        	validateForm: async function() {
                this.showMessage = false; 
                await new Promise(resolve => setTimeout(resolve, this.config.updateDelay));
            
                if (!this.leadTitle) {
                    this.showMessage = true;
                    this.messageType = 'warning';
                    this.messageContent = this.i18n.enterTitle;
                    return false;
                }
            
                const activeElement = document.activeElement;
                
                if (activeElement && activeElement.textContent === this.i18n.autoFetch) {
                    return true;
                }
                
                if (!this.leadSummary) {
                    this.showMessage = true;
                    this.messageType = 'warning';
                    this.messageContent = this.i18n.enterSummary;
                    return false;
                }
                
                return true;
            },
            
            handleUpdateClick: async function() {
                const isValid = await this.validateForm();
                if (!isValid) {
                    return;
                }
            
                if (this.explicitUpdate && !this.validateMoveCommand()) {
                    this.showMessage = true;
                    this.messageType = 'warning';
                    this.messageContent = this.moveCommandError;
                    return;
                }
            
                this.showConfirmDialog = true;
            },
            autoFetch: async function() {
			    this.resetMessageState();
			    
			    if (!await this.validateForm()) {
			        return;
			    }
			
			    this.isLoading = true;
			    this.showMessage = true;
			    this.messageType = 'notice';
			    this.messageContent = this.i18n.searching;
			
			    try {
			        let pageTitle;
			        let content;
			        const leadMatch = this.leadTitle.match(/^Lead([1-5])$/i);
			        const mlMatch = this.leadTitle.match(/^ML([1-5])$/i);
			        
			        if (leadMatch && (parseInt(leadMatch[1]) < 1 || parseInt(leadMatch[1]) > 5)) {
			            throw new Error('Lead number must be between 1 and 5');
			        }
			        if (mlMatch && (parseInt(mlMatch[1]) < 1 || parseInt(mlMatch[1]) > 5)) {
			            throw new Error('ML number must be between 1 and 5');
			        }
			
			        if (leadMatch) {
			            const leadNumber = parseInt(leadMatch[1]);
			            
			            if (!leadManager.config.leadPages[leadNumber - 1]) {
			                throw new Error(`Invalid lead page configuration for Lead${leadNumber}`);
			            }
			
			            try {
			                const response = await leadManager.api.get({
			                    action: 'query',
			                    format: 'json',
			                    prop: 'revisions',
			                    titles: leadManager.config.leadPages[leadNumber - 1],
			                    formatversion: '2',
			                    rvprop: 'content',
			                    rvslots: '*'
			                });
			
			                if (!response?.query?.pages?.[0]) {
			                    throw new Error('Invalid API response structure');
			                }
			
			                const page = response.query.pages[0];
			                if (page.missing) {
			                    throw new Error(this.i18n.pageNotFound);
			                }
			
			                content = page.revisions?.[0]?.slots?.main?.content;
			                if (!content) {
			                    throw new Error('No content found in page revision');
			                }
			
			                const templateRegex = /\{\{Lead 2\.0\s*\n([\s\S]*?)\}\}/;
			                const templateMatch = content.match(templateRegex);
			                
			                if (templateMatch) {
			                    const templateContent = templateMatch[1];
			                    
			                    const getParam = (param) => {
			                        const regex = new RegExp(`\\|\\s*${param}\\s*=\\s*([^\\n|]+)`);
			                        const match = templateContent.match(regex);
			                        return match ? match[1].trim() : '';
			                    };
			
			                    this.leadImage = this.sanitizeInput(getParam('image'));
			                    this.leadWidth = this.sanitizeInput(getParam('width')) || leadManager.config.defaultImageSize;
			                    this.leadType = this.validateType(getParam('type'));
			                    this.leadTitle = this.sanitizeInput(getParam('title'));
			                    this.leadShortTitle = this.sanitizeInput(getParam('short_title'));
			                    this.leadSummary = this.sanitizeInput(getParam('summary'));
			                    
			                    this.showSuccess();
			                    return;
			                }
			            } catch (error) {
			                console.error('Error fetching lead template:', error);
			                throw new Error(`Error fetching lead template: ${error.message}`);
			            }
			        }
			
			        // Handle ML fetch
			        if (mlMatch) {
			            const mlNumber = parseInt(mlMatch[1]);
			            
			            try {
			                const categoryResponse = await leadManager.api.get({
			                    action: 'query',
			                    format: 'json',
			                    list: 'categorymembers',
			                    cmtitle: `Category:${this.config.categoryName}`,
			                    cmsort: 'timestamp',
			                    cmdir: 'desc',
			                    cmlimit: mlNumber,
			                    formatversion: '2'
			                });
			
			                if (!categoryResponse?.query?.categorymembers) {
			                    throw new Error('Invalid category API response');
			                }
			
			                if (categoryResponse.query.categorymembers.length < mlNumber) {
			                    throw new Error(`Not enough articles found in the category. Requested: ${mlNumber}, Found: ${categoryResponse.query.categorymembers.length}`);
			                }
			
			                pageTitle = categoryResponse.query.categorymembers[mlNumber - 1].title;
			                this.leadTitle = pageTitle;
			            } catch (error) {
			                console.error('Error fetching category members:', error);
			                throw new Error(`Error fetching category members: ${error.message}`);
			            }
			        }
			
			        // Fetch article content
			        if (mlMatch || (!leadMatch && !mlMatch)) {
                        pageTitle = this.leadTitle;
            
                        try {
                            const response = await leadManager.api.get({
                                action: 'query',
                                format: 'json',
                                prop: 'revisions|info',
                                titles: pageTitle,
                                formatversion: '2',
                                rvprop: 'content',
                                rvslots: '*',
                                redirects: 1
                            });
            
                            if (!response?.query?.pages?.[0]) {
                                throw new Error('Invalid API response structure');
                            }
            
                            const page = response.query.pages[0];
                            if (page.missing) {
                                throw new Error(this.i18n.pageNotFound);
                            }
            
                            // Update the title if it was redirected
                            if (response.query.redirects?.[0]) {
                                this.leadTitle = response.query.redirects[0].to;
                            }
            
                            content = page.revisions?.[0]?.slots?.main?.content;
                            if (!content) {
                                throw new Error('No content found in page revision');
                            }
            
                            let image = leadManager.helpers.extractFirstImage(content);
                            if (!image) {
                                image = leadManager.helpers.findBackupImage(content, this.leadTitle);
                            }
                            this.leadImage = this.sanitizeInput(image || '');
                          
                            this.leadType = this.validateType(leadManager.helpers.detectType(content));
                            
                            this.leadSummary = this.sanitizeInput(
                                leadManager.helpers.cleanupContent(content, this.sentenceCount)
                            );
                        } catch (error) {
                            console.error('Error fetching article content:', error);
                            throw new Error(`Error fetching article content: ${error.message}`);
                        }
                    }
            
                    this.showSuccess();
            
                } catch (error) {
                    console.error('Error in autoFetch:', error);
                    this.showError(error.message);
                }
            },
			
			handleConfirmDialogConfirm: async function() {
			    this.showConfirmDialog = false;
			    this.showProgress = true;
			
			    try {
			        const newLeadContent = this.buildTemplate({
			            id: this.startPosition,
			            image: this.leadImage,
			            width: this.leadWidth,
			            type: this.leadType,
			            title: this.leadTitle,
			            short_title: this.leadShortTitle,
			            summary: this.leadSummary
			        });
			
			        await this.updateLeadArticles(newLeadContent, this.startPosition, this.singleUpdate);
			
			        this.showProgress = false;
			        
			        this.showSuccessDialog = true;
			        this.showMessage = true;
			        this.messageType = 'success';
			        this.messageContent = leadManager.i18n.updateSuccess;
			        
			    } catch (error) {
			        console.error('Error updating leads:', error);
			        this.showProgress = false;
			        this.showMessage = true;
			        this.messageType = 'error';
			        this.messageContent = leadManager.i18n.updateError;
			    }
			},
			 parseImageSize: function(sizeString) {
		        const match = sizeString.match(/^([^(]+)(?:\s*\(([^)]+)\))?/);
		        if (!match) return { mainSize: sizeString, otherSize: sizeString };
		        
		        const mainSize = match[1].trim();
		        const otherSize = match[2] ? match[2].trim() : mainSize;
		        
		        return { mainSize, otherSize };
		    },
			
			updateLeadArticles: async function(newLeadContent, startPosition, singleUpdate) {
			    try {
			        if (!newLeadContent || typeof startPosition !== 'number' || startPosition < 1 || startPosition > 5) {
			            throw new Error('Invalid input parameters');
			        }
			
			        const delay = async (seconds) => {
			            return new Promise(resolve => setTimeout(resolve, seconds * 1000));
			        };
			
			        const fetchLeadContent = async (lead, retryCount = 3) => {
			            for (let i = 0; i < retryCount; i++) {
			                try {
			                    const response = await Promise.race([
			                        leadManager.api.get({
			                            action: 'query',
			                            format: 'json',
			                            prop: 'revisions',
			                            titles: lead,
			                            formatversion: '2',
			                            rvprop: 'content',
			                            rvslots: '*'
			                        }),
			                        new Promise((_, reject) => 
			                            setTimeout(() => reject(new Error('Request timeout')), 10000)
			                        )
			                    ]);
			
			                    if (!response?.query?.pages?.[0]) {
			                        throw new Error('Invalid API response structure');
			                    }
			
			                    const page = response.query.pages[0];
			                    if (!page?.revisions?.[0]?.slots?.main?.content) {
			                        throw new Error('No content found in page revision');
			                    }
			
			                    return page.revisions[0].slots.main.content;
			                } catch (error) {
			                    if (i === retryCount - 1) throw error;
			                    await delay(i + 1);
			                }
			            }
			        };
			
			        const leadContents = await Promise.all(
			            leadManager.config.leadPages.map(lead => fetchLeadContent(lead))
			        );
			
			        const { mainSize, otherSize } = this.parseImageSize(this.leadWidth);
			        if (!mainSize || !otherSize) {
			            throw new Error('Invalid image size format');
			        }
			
			        const leads = leadManager.config.leadPages;
			        let updatedContents;
			        let titleMap = new Map();
			
			        const extractTitle = (content) => {
			            const titleMatch = content.match(/\|title\s*=\s*([^\n|]+)/);
			            if (!titleMatch) {
			                throw new Error('Failed to extract title from template');
			            }
			            return titleMatch[1].trim();
			        };
			
			        if (this.explicitUpdate) {
			            if (!this.validateMoveCommand()) {
			                throw new Error(this.moveCommandError || 'Invalid move command');
			            }
			
			            updatedContents = [...leadContents];
			            const moves = this.moveCommand.split(',').map(m => m.trim());
			
			            moves.forEach(move => {
			                const [from, to] = move.split('->').map(n => parseInt(n.trim()) - 1);
			                if (isNaN(from) || isNaN(to)) {
			                    throw new Error('Invalid move command format');
			                }
			                titleMap.set(to, extractTitle(leadContents[from]));
			            });
			
			            titleMap.set(startPosition - 1, this.leadTitle);
			
			            moves.forEach(move => {
			                const [from, to] = move.split('->').map(n => parseInt(n.trim()) - 1);
			                let content = leadContents[from];
			                content = this.updateTemplateWidth(content, otherSize);
			                updatedContents[to] = content;
			            });
			
			            updatedContents[startPosition - 1] = newLeadContent;
			
			        } else if (!singleUpdate) {
			            updatedContents = [...leadContents];
			
			            for (let i = leads.length - 1; i > startPosition - 1; i--) {
			                titleMap.set(i, extractTitle(leadContents[i - 1]));
			            }
			            titleMap.set(startPosition - 1, this.leadTitle);
			
			            for (let i = leads.length - 1; i > startPosition - 1; i--) {
			                let content = leadContents[i - 1];
			                content = this.updateTemplateWidth(content, otherSize);
			                updatedContents[i] = content;
			            }
			            updatedContents[startPosition - 1] = newLeadContent;
			
			        } else {
			            updatedContents = [...leadContents];
			            updatedContents[startPosition - 1] = newLeadContent;
			            titleMap.set(startPosition - 1, this.leadTitle);
			        }
			
			        // Update template IDs
			        updatedContents = updatedContents.map((content, index) => {
			            return this.updateTemplateId(content, index + 1);
			        });
			
			        // Create edit summaries
			        const createEditSummary = (index) => {
			            const leadNumber = `Lead${index + 1}`;
			            const title = titleMap.get(index) || this.leadTitle;
			            return `Updating ${leadNumber} with [[${title}]] ([[User:Asked42/LeadManager|Lead Manager]])`;
			        };
			
			        for (let i = 0; i < leads.length; i++) {
			            let retryCount = 3;
			            while (retryCount > 0) {
			                try {
			                    await leadManager.api.postWithToken('csrf', {
			                        action: 'edit',
			                        format: 'json',
			                        title: leads[i],
			                        formatversion: '2',
			                        nocreate: 1,
			                        text: updatedContents[i],
			                        summary: createEditSummary(i)
			                    });
			                    break;
			                } catch (error) {
			                    retryCount--;
			                    if (retryCount === 0) {
			                        throw new Error(`Failed to update ${leads[i]}: ${error.message}`);
			                    }
			                    await delay(1);
			                }
			            }
			        }
			    } catch (error) {
			        console.error('Error in updateLeadArticles:', error);
			        throw error;
			    }
			},
			
			sanitizeInput: function(input) {
			    if (!input) return '';
			    return input.trim()
			        .replace(/[<>]/g, '')  
			        .replace(/^\s+|\s+$/g, '');
			},
			
			validateType: function(type) {
			    const validTypes = ['none', 'breaking-news', 'special-report', 'original-report', 'exclusive-interview'];
			    return validTypes.includes(type) ? type : 'none';
			},
			
			updateTemplateWidth: function(content, width) {
			    return content.replace(/\|width\s*=\s*[^|\n]+/, `|width = ${width}`);
			},
			
			updateTemplateId: function(content, id) {
			    return content.replace(/(\|id\s*=\s*)\d+/g, `$1${id}`);
			},
			
			showSuccess: async function() {
			    this.showMessage = false;
			    await new Promise(resolve => setTimeout(resolve, this.config.updateDelay));
			    
			    this.isLoading = false;
			    this.showMessage = true;
			    this.messageType = 'success';
			    this.messageContent = this.leadImage ? this.i18n.fetchSuccess : this.i18n.fetchDescriptionSuccess;
			},
			
			showError: async function(message) {
			    this.showMessage = false;
			    await new Promise(resolve => setTimeout(resolve, this.config.updateDelay));
			    
			    this.isLoading = false;
			    this.showMessage = true;
			    this.messageType = 'error';
			    this.messageContent = this.i18n.fetchError + message;
			},
            resetForm: function() {
		        this.leadImage = '';
		        this.sentenceCount = 2;
		        this.leadWidth = leadManager.config.defaultImageSize,
		        this.leadType = 'none';
		        this.leadTitle = '';
		        this.leadShortTitle = '';
		        this.leadSummary = '';
		        this.startPosition = 1;
		        this.singleUpdate = false;
		        this.explicitUpdate = false;
		        this.moveCommand = '',
		        this.moveCommandError = '';
		        this.showMessage = false;
		        this.messageType = '';
		        this.messageContent = '';
		        this.showConfirmDialog = false;
		        this.showSuccessDialog = false;
		        this.showProgress = false;
		        this.isLoading = false;
		    },
            resetMessageState: function() {
                this.showMessage = false;
                this.messageType = '';
                this.messageContent = '';
            },
            handleMessageDismiss: function() {
                this.showMessage = false;
            },
            handleSuccessDialogClose: function() {
                this.showSuccessDialog = false;
                window.location.reload();
            },
            handleConfirmDialogCancel: function() {
                this.showConfirmDialog = false;
            },

        buildTemplate: function(params) {
        const { mainSize } = this.parseImageSize(params.width);
            return `{{Lead 2.0
 |id    = ${params.id} <!-- Do not change. Each lead must have its own unique ID -->
 |image = ${params.image}
 |width = ${mainSize}
 |type  = ${params.type}
 |title = ${params.title}
 |short_title = ${params.short_title}
 |summary     = ${params.summary}
}}<noinclude>{{Lead article doc}}</noinclude>`;
        },
            validateMoveCommand: function() {
			    this.moveCommandError = '';
			    
			    if (!this.explicitUpdate || !this.moveCommand.trim()) {
			        return true;
			    }
			
			    const moves = this.moveCommand.split(',').map(m => m.trim());
			    const usedSources = new Set();
			    const usedDestinations = new Set();
			    const templateCount = 5;
			
			    const firstMove = moves[0].split('->').map(n => parseInt(n.trim()));
			    if (firstMove[0] !== this.startPosition) {
			        this.moveCommandError = `First move must start from selected lead template number, which is ${this.startPosition}.`;
			        return false;
			    }
			
			    for (const move of moves) {
			        if (!/^\d+\s*->\s*\d+$/.test(move)) {
			            const invalidPart = move.includes('->') ? 
			                move.split('->').find(part => !/^\s*\d+\s*$/.test(part)) :
			                move;
			            this.moveCommandError = `Invalid syntax in "${move}". Correct format: "1->2, 2->3". Issue with: "${invalidPart}"`;
			            return false;
			        }
			
			        const [from, to] = move.split('->').map(n => parseInt(n.trim()));
			
			        if (from < 1 || from > templateCount || to < 1 || to > templateCount) {
			            const invalidNum = from < 1 || from > templateCount ? from : to;
			            this.moveCommandError = `Invalid lead number "${invalidNum}". Must be between 1 and ${templateCount}`;
			            return false;
			        }
			
			        if (usedSources.has(from)) {
			            this.moveCommandError = `Lead ${from} is moved multiple times. Each lead can only be moved once`;
			            return false;
			        }
			        if (usedDestinations.has(to)) {
			            this.moveCommandError = `Multiple templates moved to lead ${to}. Each lead can only receive one template`;
			            return false;
			        }
			
			        usedSources.add(from);
			        usedDestinations.add(to);
			    }
			
			    for (const source of usedSources) {
			        if (!usedDestinations.has(source) && source !== this.startPosition) {
			            this.moveCommandError = `Lead ${source} would be empty after moves. Ensure all leads are filled`;
			            return false;
			        }
			    }
			
			    return true;
			},
		    generateMovePreview: function() {
			    const totalTemplates = leadManager.config.leadPages.length;
			    const newContentTitle = this.leadTitle || 'New content';
			    let preview = {
			        moves: [],
			        method: ''
			    };
			
			    try {
			        if (this.explicitUpdate && this.moveCommand.trim()) {
			            preview.method = 'Dynamic: Explicit Movement';
			            
			            const moves = this.moveCommand.split(',').map(m => {
			                const [from, to] = m.trim().split('->').map(n => parseInt(n.trim()));
			                return {
			                    from: from,
			                    to: to,
			                    isNew: from === this.startPosition
			                };
			            });
			
			            moves.forEach(move => {
			                const moveText = ` Lead ${move.from} → Lead ${move.to}: Content will be moved`;
			                preview.moves.push(moveText);
			            });
			            
			            preview.moves.push(`NEW CONTENT: Lead ${this.startPosition} with "${newContentTitle}"`);
			
			        } else if (this.singleUpdate) {
			            preview.method = 'Static: Single Template Update';
			            preview.moves.push(`NEW CONTENT: Lead ${this.startPosition} with "${newContentTitle}"`);
			
			        } else {
			            preview.method = 'Dynamic: Sequential Movement';
			            
			            for (let i = totalTemplates; i > this.startPosition; i--) {
			                preview.moves.push(`ℹ️ Lead ${i-1} → Lead ${i}: Content will be moved`);
			            }
			            preview.moves.push(`NEW CONTENT: Lead ${this.startPosition} with "${newContentTitle}"`);
			        }
			
			    } catch (error) {
			        console.error('Error in generateMovePreview:', error);
			        preview.moves = ['Unable to generate movement preview'];
			        preview.method = 'Error';
			    }
			
			    return {
			        summary: [
			            `Update Method: ${preview.method}`,
			            'Template Shuffle Preview:',
			            preview.moves.join('\n')
			        ].join('\n'),
			        method: preview.method,
			        moves: preview.moves.join('\n')
			    };
			},
        },
        template: `
            <div style="max-width: 500px; margin: 20px auto; padding: 20px; border: 1px solid #c8ccd1; border-radius: 0; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);">
                <cdx-label :input-id="'ml_title'">{{ i18n.title }}</cdx-label>
                <div style="margin-bottom: 15px;">
                    <cdx-text-input id="ml_title" v-model="leadTitle" :placeholder="i18n.titlePlaceholder" required style="flex-grow: 1;" />
                </div>

                <cdx-label :input-id="'ml_sentences'">{{ i18n.sentences }}</cdx-label>
                <div style="display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap;">
                    <cdx-select 
                        id="ml_sentences" 
                        v-model:selected="sentenceCount" 
                        :menu-items="sentenceOptions" 
                        style="flex-grow: 1; min-width: 200px;"
                    />
                    <cdx-button @click="autoFetch" action="normal" :disabled="isLoading" :style="{ minWidth: config.buttonMinWidth }">
                        {{ i18n.autoFetch }}
                    </cdx-button>
                </div>

                <cdx-label :input-id="'ml_image'">{{ i18n.image }}</cdx-label>
                <cdx-text-input id="ml_image" v-model="leadImage" :placeholder="i18n.imagePlaceholder" style="margin-bottom: 15px;" />

                <cdx-label :input-id="'ml_summary'">{{ i18n.summary }}</cdx-label>
                <cdx-textarea id="ml_summary" v-model="leadSummary" :aria-label="i18n.summary" :placeholder="i18n.summaryPlaceholder" rows="8" required style="margin-bottom: 15px;" />

                <cdx-accordion>
                    <template #title>{{ i18n.additionalOptions }}</template>
                    <template #description>{{ i18n.optionsDescription }}</template>
                    <div style="font-size: 14px; margin-top: 20px;">
                        <cdx-label :input-id="'ml_width'">{{ i18n.imageSize }}</cdx-label>
						<cdx-text-input 
						    id="ml_width" 
						    v-model="leadWidth" 
						    :placeholder="config.defaultImageSize" 
						    style="margin-bottom: 5px;" 
						/>
						<div style="font-size: 12px; color: #54595d; margin-bottom: 15px;">
						    Format: {{ i18n.imageSizeFormat }}
						</div>
                        <cdx-label :input-id="'ml_type'">{{ i18n.newsType }}</cdx-label>
                        <cdx-select id="ml_type" v-model:selected="leadType" :menu-items="typeOptions" style="margin-bottom: 15px;" />
                        
                        <cdx-label :input-id="'ml_short_title'">{{ i18n.shortTitle }}</cdx-label>
                        <cdx-text-input id="ml_short_title" v-model="leadShortTitle" :placeholder="i18n.shortTitlePlaceholder" style="margin-bottom: 15px;" />
                        
                        <cdx-label :input-id="'ml_position'">{{ i18n.leadTemplate }}</cdx-label>
                        <cdx-select id="ml_position" v-model:selected="startPosition" :menu-items="positionOptions" style="margin-bottom: 15px;" />
                        
                        <cdx-checkbox 
                            v-model="singleUpdate" 
                            :disabled="explicitUpdate"
                            style="margin-bottom: 15px;"
                        >
                            {{ i18n.singleUpdateLabel }}
                            <template #description>{{ i18n.singleUpdateDesc }}</template>
                        </cdx-checkbox>
            
                        <cdx-checkbox 
                            v-model="explicitUpdate" 
                            :disabled="singleUpdate"
                            style="margin-bottom: 15px;"
                        >
                            {{ i18n.explicitUpdateLabel }}
                            <template #description>{{ i18n.explicitUpdateDesc }}</template>
                        </cdx-checkbox>
            
                        <template v-if="explicitUpdate">
					        <cdx-label :input-id="'ml_move_command'">{{ i18n.moveCommandLabel }}</cdx-label>
					        <div class="move-command-container" style="position: relative;">
					            <cdx-text-input 
					                id="ml_move_command"
					                v-model="moveCommand"
					                :placeholder="moveCommandPlaceholder"
					                style="margin-bottom: 5px; font-family: monospace;"
					            />
					            <div class="move-command-help" style="font-size: 12px; color: #54595d; margin-bottom: 15px; font-family: monospace;">
					                <strong>Format:</strong> {{ i18n.explicitUpdateFormat }} <br/>
					                <strong>{{ i18n.explicitUpdateExamples }} </strong><br/>
					                {{ i18n.explicitUpdateExample1 }}<br/>
					                {{ i18n.explicitUpdateExample2 }}<br/>
					                {{ i18n.explicitUpdateExample3 }}<br/>
					                <div v-if="moveCommandError" class="error-message" style="color: #d33; margin-top: 5px;">
					                    {{ moveCommandError }}
					                </div>
					            </div>
					        </div>
					    </template>
                    </div>
                </cdx-accordion>

                <div style="display: flex; justify-content: space-between; margin-top: 15px;">
                    <cdx-button 
                        @click="handleUpdateClick" 
                        action="progressive" 
                        weight="primary" 
                        style="margin-bottom: 15px; margin-right: 5px;"
                    >
                        {{ i18n.startUpdate }}
                    </cdx-button>
                    <cdx-button @click="resetForm" action="normal" style="margin-bottom: 15px;">
                        {{ i18n.reset }}
                    </cdx-button>
                </div>

                <div style="margin: 15px 0;">
			        <cdx-progress-bar v-if="showProgress" inline />
			    </div>
            </div>

            <cdx-dialog v-if="showConfirmDialog" :open="showConfirmDialog" @close="handleConfirmDialogCancel" :title="i18n.updateConfirmTitle">
                <template #default>
		            <p>{{ i18n.confirmUpdateMessage }}</p>
		            <div style="margin-top: 15px;">
		                <p style="font-weight: bold; margin-bottom: 5px;">Update Method: {{ generateMovePreview().method }}</p>
		                <p style="font-weight: bold; margin-bottom: 5px;">{{ i18n.templateShufflePreview }}</p>
		                <pre style="white-space: pre-line; margin: 10px 0; padding: 10px; background-color: #f8f9fa; border: 1px solid #eaecf0; border-radius: 2px;">{{ generateMovePreview().moves }}</pre>
		            </div>
		        </template>
                <template #footer>
                    <cdx-button action="progressive" weight="primary" @click="handleConfirmDialogConfirm" style="margin-right: 10px;">
                        {{ i18n.confirm }}
                    </cdx-button>
                    <cdx-button action="normal" @click="handleConfirmDialogCancel">
                        {{ i18n.cancel }}
                    </cdx-button>
                </template>
            </cdx-dialog>
        
            <cdx-dialog v-if="showSuccessDialog" :open="showSuccessDialog" @close="handleSuccessDialogClose" :title="i18n.updateSuccessTitle">
                <template #default>
                    <p>{{ i18n.updateSuccess }}</p>
                </template>
                <template #footer>
                    <cdx-button action="progressive" weight="primary" @click="handleSuccessDialogClose">
                        {{ i18n.ok }}
                    </cdx-button>
                </template>
            </cdx-dialog>
        
            <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="messageType === 'success' || messageType === 'notice'"
                    :display-time="config.messageDisplayTime"
                    dismiss-button-label="Close"
                    @dismissed="handleMessageDismiss"
                >
                    {{ messageContent }}
                </cdx-message>
            </div>
        `
    })
    .component('cdx-text-input', Codex.CdxTextInput)
    .component('cdx-textarea', Codex.CdxTextArea)
    .component('cdx-select', Codex.CdxSelect)
    .component('cdx-checkbox', Codex.CdxCheckbox)
    .component('cdx-button', Codex.CdxButton)
    .component('cdx-progress-bar', Codex.CdxProgressBar)
    .component('cdx-message', Codex.CdxMessage)
    .component('cdx-dialog', Codex.CdxDialog)
    .component('cdx-label', Codex.CdxLabel)
    .component('cdx-accordion', Codex.CdxAccordion)
    .mount('#lead-manager-container');
});

// </nowiki>