Greasy Fork is available in English.

Click ChatGPT Last Sentence to Continue

To continue the last response with one click.

// ==UserScript==
// @name         Click ChatGPT Last Sentence to Continue
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  To continue the last response with one click.
// @author       CY Fung
// @match        https://chat.openai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @grant        none
// @license      MIT
// @run-at       document-idle
// @noframes
// ==/UserScript==

'use strict';
(function () {
    'use strict';

    let allLen = 0;
    let cid1 = 0;

    function lastOfArr(elements) {
        return elements && elements.length >= 1 ? elements[elements.length - 1] : null;
    }

    function addStyleText(t) {
        let s = document.createElement('style');
        s.textContent = t;
        document.head.appendChild(s);
        return s;
    }

    function getSelectableRows(element) {
        let childNodes = [];
        let nodeA = element.firstChild;
        while (nodeA instanceof Node) {
            let node = nodeA;
            if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.tagName === 'P') {
                    //
                } else if (node.tagName === 'OL' || node.tagName === 'UL') {
                    node = lastOfArr(node.querySelectorAll('li'));
                } else if (node.tagName === 'PRE') {
                    node = lastOfArr(node.querySelectorAll('code'));
                } else {
                    node = null;
                }
                if (node) {
                    let textContent = node.textContent;
                    if (textContent.length >= 1 && textContent.trim().length >= 1) {
                        childNodes.push(node);
                    }
                }
            }
            nodeA = nodeA.nextSibling;
        }
        return childNodes;
    }

    let previousRow = null;

    const cssStyle = `
    .last-row-message.message-stopped:hover{
      background: rgba(125,125,125,0.25);
      cursor:pointer;
    }
    `;

    async function setTextAreaMessage(textarea, message) {
        textarea.focus();
        await new Promise(window.requestAnimationFrame);
        textarea.value = '';
        if (message) {
            await new Promise(window.requestAnimationFrame);
            textarea.value = message;
        }
        await new Promise(window.requestAnimationFrame);
        try {
            document.execCommand("insertText", false, " ")
        } catch (e) { }
    }

    function getRandomElement(array) {
        let randomIndex = Math.floor(Math.random() * array.length);
        return array[randomIndex];
    }

    function preMultilineTextProcess(text) {
        let lines = text.split('\n');
        for(let i =lines.length-1; i>=0; i--){
            let noSymbolLine = lines[i].replace(/[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\x80-\xFF]+/g,'').trim();
            // exclude symbols
            // remains number, english letters, chinese, ....
            if(noSymbolLine.length>0){
                let foundAt = i;
                text = lines.slice(foundAt).join('\n').trim();
                return text;
            }
        }
        return text;
    }

    function howToSay(s) {
        if (!s) return null;
        s = s.trim().replace(/(\r\n|\n\r|\r)/g,'\n');
        s = preMultilineTextProcess(s);
        let sk = 0;
        if (s.includes('\n')) sk = 3;
        else if (s.includes('```') || s.includes('`')) sk = 3;
        else if (s.includes('\"')) sk = 2;
        else sk = 1;

        if (sk == 3) {
            s = '```\n' + s.replace(/\`\`\`/g, '\n\n') + '\n```';
        } else if (sk == 2) {
            s = '`' + s + '`';
        } else if (sk == 1) {
            s = '"' + s + '"';
        }
        let arr = [
            // `Your last response stopped at ${s}. Please continue.`,
            `Please continue on your last response which stopped at ${s}.`,
            `Continue on your last response from ${s}.`,
            `Please resume your last response which paused at ${s}.`,
            `Resume your last response from ${s}.`,

            // `Your previous response stopped at ${s}. Please continue.`,
            `Please continue on your previous response which stopped at ${s}.`,
            `Continue on your previous response from ${s}.`,
            `Please resume your previous response which paused at ${s}.`,
            `Resume your previous response from ${s}.`,
        ];
        return getRandomElement(arr);
    }


    function howToSayCode() {
        let arr = [
            `Please continue the coding with the last incomplete line that you left off in your last response.`,
            `Please continue the coding with the last incomplete line that you left off in your previous response.`,
            `Please continue the coding with the last incomplete line that you abruptly ended in your last response.`,
            `Please continue the coding with the last incomplete line that you abruptly ended in your previous response.`
        ];
        return getRandomElement(arr);
    }

    function clickLastRowMessageHandler(evt) {
        if (!evt || !evt.target) return;
        let target = evt.target.closest('.last-row-message.message-stopped');
        if (!target) return;
        let p = target;
        let textarea = null;
        while (p = p.parentNode) {
            if (textarea = p.querySelector('textarea[placeholder]')) break;
        }
        if (textarea !== null) {
            evt.preventDefault();
            let display = '';
            if (target.closest('code')) {
                display = howToSayCode();
            } else {
                display = howToSay(target.textContent);
            }
            setTextAreaMessage(textarea, display);
        }
    }

    let newMessageFoundResult = null;

    const newMessageFoundTFunc = () => {
        let message = newMessageFoundResult;
        if(message && message.isConnected === false){
            newMessageFoundResult = null;
            clearCid1(); // removed
            allLen = 0;
            checkNewMessage();
            return;
        }
        if (message === null) return;
        let selectableRows = getSelectableRows(message);
        let lastRow = selectableRows[selectableRows.length - 1] || null;
        if(lastRow === null && message.lastChild instanceof Text){
            // this could happen when GPT's response is purely a text without formatting. (Least Chance)
            lastRow = message;
        }

        if (lastRow !== previousRow && lastRow) {
            let mRow = previousRow;
            previousRow = lastRow;
            if (mRow !== null) {
                mRow.classList.remove('last-row-message', 'message-stopped', 'message-checking');
                mRow.removeAttribute('title');
                mRow.removeEventListener('click', clickLastRowMessageHandler, false);
            }
            lastRow.classList.add('last-row-message', 'message-checking');
            lastRow.removeAttribute('title');
            lastRow.classList.remove('message-stopped');

            lastRow.addEventListener('click', clickLastRowMessageHandler, false);
            if (mRow === null) {
                let lastRowCss= document.querySelector('#last-row-css');
                if(lastRowCss === null){
                    let style = addStyleText(cssStyle);
                    style.id = 'last-row-css'
                }
            }
            rowText = '';
        }
    };

    function clearCid1(){
        if (cid1 > 0) clearInterval(cid1);
        cid1 = 0;
    }

    function newMessageFound(message) {
        clearCid1();
        if (!message) return;
        newMessageFoundResult = message;
        cid1 = setInterval(newMessageFoundTFunc, 100);
    }

    let counter = 0;
    let rowText = '';
    let di = 0;
    let lastStreamState = false; // as there are some bugs for the detection, reset once result-streaming is flipped.

    function checkNewMessage() {
        let all = document.querySelectorAll('div.markdown.prose:not(:empty)');
        let cLen = all.length;
        if (cLen !== allLen) {
            allLen = cLen;
            if (allLen >= 1) newMessageFound(all[all.length - 1]);
        }
    }

    function reset() {
        previousRow = null;
        rowText = '';
        counter = 0;
    }

    setInterval(() => { // 30ms
        let oldLastStreamState = lastStreamState;
        let newLastStreamState = false;
        
        di++;
        if ((di % 10) === 0) { // 300ms
            if (di > 900) di = 0;
            checkNewMessage();
        }

        if (previousRow !== null && previousRow.isConnected === false) {
            previousRow = null;
        }

        if (previousRow !== null) {
            if (previousRow.closest('.result-streaming')) {
                newLastStreamState = true;
            } else {
                let text = previousRow.textContent;
                if (rowText !== text) {
                    let mText = rowText;
                    rowText = text;
                    counter = 0;
                    let k = 3;
                    let g = mText.length - k;
                    if (g > 0 && text.length > mText.length && text.substring(0, g) === mText.substring(0, g) && previousRow.classList.contains('message-checking')) {
                        previousRow.classList.remove('message-checking');
                    }
                }
                if (counter < 96) counter++;
                if (counter === 20) {
                    previousRow.classList.add('message-stopped');
                    previousRow.classList.remove('message-checking');

                    previousRow.setAttribute('title', 'click to ask continue');
                }
            }
        }

        if (oldLastStreamState !== newLastStreamState) {
            lastStreamState = newLastStreamState;
            reset();
        }

    }, 30);

    // Your code here...
})();