c.AI Search Sort

Sort search so cards with public definition stays on top and marked with a star

目前為 2024-10-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name         c.AI Search Sort
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Sort search so cards with public definition stays on top and marked with a star
// @author       EnergoStalin
// @license      GPL-3.0-or-later
// @match        https://character.ai/search*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=character.ai
// @grant        none
// ==/UserScript==

(async function() {
    'use strict';

    async function waitNotNull(func, timeout = 10000, interval = 1000) {
        return new Promise((res, rej) => {
            let time = timeout
            const i = setInterval(async () => {
                const c = await func()
                time -= interval
                if (time <= 0) {
                    clearInterval(i)
                    rej()
                }
                if (!c) return

                clearInterval(i)
                res(c)
            }, interval)
        })
    }

    const [pageProps, cardsContainer] = await Promise.all([
        waitNotNull(() => document.querySelector('#__NEXT_DATA__')).then(e => JSON.parse(e.textContent).props.pageProps),
        waitNotNull(() => document.evaluate('/html/body/div[1]/div/main/div/div/div/main/div/div[2]', document).iterateNext())
    ]);
    const token = pageProps.token

    async function isDefinitionPublic(id) {
        const character = await fetch(`https://plus.character.ai/chat/character/info/`, {
            headers: {
                'Authorization': `Token ${token}`,
                'Origin': 'https://character.ai/',
                'Referer': 'https://character.ai/',
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            method: 'POST',
            body: JSON.stringify({ external_id: id })
        })
        .then(e => e.json())
        .then(e => e.character)
        return !!character.definition
    }

    function clearStatus(card) { card.removeChild(card.querySelector('div[data-status]')) }

    function isStarred(card) { return !!card.querySelector('div[data-status="starred"]') }
    function setStarredStatus(card) {
        card.innerHTML += `
            <div data-status="starred" class="relative" style="min-height: 90px;">
                <svg style="margin-top: 10px; margin-right: 10px;" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#75FB4C"><path d="M371.01-324 480-390.22 589-324l-29-124 97-84-127-11-50-117-50 117-127 11 96.89 83.95L371.01-324ZM480-72 360-192H192v-168L72-480l120-120v-168h168l120-120 120 120h168v168l120 120-120 120v168H600L480-72Zm0-102 90-90h126v-126l90-90-90-90v-126H570l-90-90-90 90H264v126l-90 90 90 90v126h126l90 90Zm0-306Z"/></svg>
            </div>
        `
    }

    function setPendingStatus(card) {
        card.innerHTML += `
            <div data-status="pending" class="relative" style="min-height: 90px;">
                <svg style="margin-top: 10px; margin-right: 10px;" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#5985E1"><path d="M288-420q25 0 42.5-17.5T348-480q0-25-17.5-42.5T288-540q-25 0-42.5 17.5T228-480q0 25 17.5 42.5T288-420Zm192 0q25 0 42.5-17.5T540-480q0-25-17.5-42.5T480-540q-25 0-42.5 17.5T420-480q0 25 17.5 42.5T480-420Zm192 0q25 0 42.5-17.5T732-480q0-25-17.5-42.5T672-540q-25 0-42.5 17.5T612-480q0 25 17.5 42.5T672-420ZM480.28-96Q401-96 331-126t-122.5-82.5Q156-261 126-330.96t-30-149.5Q96-560 126-629.5q30-69.5 82.5-122T330.96-834q69.96-30 149.5-30t149.04 30q69.5 30 122 82.5T834-629.28q30 69.73 30 149Q864-401 834-331t-82.5 122.5Q699-156 629.28-126q-69.73 30-149 30Zm-.28-72q130 0 221-91t91-221q0-130-91-221t-221-91q-130 0-221 91t-91 221q0 130 91 221t221 91Zm0-312Z"/></svg>
            </div>
        `
    }

    const cardsObserver = new MutationObserver(sortSearches)
    function sortSearches() {
        cardsObserver.disconnect()
        const nodes = Array.from(cardsContainer.childNodes)

        Promise.all(nodes.map(async card => {
            if(isStarred(card)) return

            setPendingStatus(card)
            const isPublic = await isDefinitionPublic(card.href.split('/').pop())
            clearStatus(card)

            if(isPublic) {
                setStarredStatus(card)
            } else {
                cardsContainer.appendChild(card)
            }
        })).then(() => cardsObserver.observe(cardsContainer, { attributes: false, childList: true, subtree: false }))
    }

    sortSearches()
})();