Duck.AI Chat Search 🔎 (DuckDuckGo's AI)

Adds a chat search bar to Duck.AI so you can easily search messages in your conversations.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name:tr     Duck.AI Sohbet Arama 🔎 (DuckDuckGo’nun Yapay Zekası)
// @name:fr     Recherche de Chat Duck.AI 🔎 (IA de DuckDuckGo)
// @name:id     Pencarian Chat Duck.AI 🔎 (AI dari DuckDuckGo)
// @name:pt-BR  Pesquisa de Chat Duck.AI 🔎 (IA do DuckDuckGo)
// @name:es     Búsqueda de Chat Duck.AI 🔎 (IA de DuckDuckGo)
// @name:pl     Wyszukiwarka Czatów Duck.AI 🔎 (AI DuckDuckGo)
// @name:vi     Tìm kiếm Chat Duck.AI 🔎 (AI của DuckDuckGo)
// @name:uk     Пошук чатів Duck.AI 🔎 (ШІ від DuckDuckGo)
// @name:it     Ricerca Chat Duck.AI 🔎 (IA di DuckDuckGo)
// @name:nl     Duck.AI Chatzoeker 🔎 (AI van DuckDuckGo)
// @name:ru     Поиск чатов Duck.AI 🔎 (ИИ от DuckDuckGo)
// @name:ja     Duck.AI チャット検索 🔎(DuckDuckGo の AI)
// @name        Duck.AI Chat Search 🔎 (DuckDuckGo's AI)
// @name:ko     Duck.AI 채팅 검색기 🔎 (DuckDuckGo의 AI)
// @name:zh-CN  Duck.AI 聊天搜索 🔎(DuckDuckGo 的 AI)
// @name:de     Duck.AI Chatsuche 🔎 (DuckDuckGo KI)
// @description:it Aggiunge una barra di ricerca alla chat Duck.AI per trovare facilmente i messaggi nelle conversazioni.
// @description:fr Ajoute une barre de recherche à Duck.AI pour rechercher facilement des messages dans vos discussions.
// @description:pt-BR Adiciona uma barra de pesquisa ao Duck.AI para facilitar a busca de mensagens nas conversas.
// @description:tr Duck.AI sohbetine bir arama çubuğu ekler; böylece önceki mesajlarınızı kolayca arayabilirsiniz.
// @description:id Menambahkan bilah pencarian ke Duck.AI untuk memudahkan pencarian pesan dalam obrolan Anda.
// @description:pl Dodaje pasek wyszukiwania do Duck.AI, umożliwiając łatwe wyszukiwanie wiadomości w czatach.
// @description:vi Thêm thanh tìm kiếm vào Duck.AI để bạn dễ dàng tìm lại các tin nhắn trong cuộc trò chuyện.
// @description:nl Voegt een zoekbalk toe aan Duck.AI waarmee je eenvoudig berichten in je chats kunt zoeken.
// @description Adds a chat search bar to Duck.AI so you can easily search messages in your conversations.
// @description:es Añade una barra de búsqueda a Duck.AI para encontrar fácilmente mensajes en tus chats.
// @description:ru Добавляет строку поиска в Duck.AI, чтобы вы могли легко находить сообщения в чатах.
// @description:de Fügt Duck.AI eine Suchleiste hinzu, um Nachrichten in Chats einfach zu finden.
// @description:ja Duck.AI に検索バーを追加し、チャット内のメッセージを簡単に検索できるようにします。
// @description:uk Додає панель пошуку в Duck.AI, щоб легко знаходити повідомлення у чатах.
// @description:ko Duck.AI 채팅에 검색창을 추가하여 이전 메시지를 쉽게 찾을 수 있습니다.
// @description:zh-CN 为 Duck.AI 聊天添加搜索栏,让你轻松搜索聊天中的消息内容。
// @require     https://cdn.jsdelivr.net/npm/[email protected]
// @supportURL  https://github.com/Hakorr/Userscripts
// @match       https://duckduckgo.com/*duckai*
// @run-at      document-load
// @grant       GM_addStyle
// @namespace   HKR
// @author      HKR
// @version     1.1
// ==/UserScript==

const fuseOptions = {
    includeScore: true,
    includeMatches: true,
    threshold: 0.2,
    ignoreLocation: true,
    distance: Infinity,
    keys: [
        'title',
        'messages.content',
        'messages.parts.text'
    ],
};

function truncate(str, maxLength, fromStart = false) {
    if(str.length <= maxLength) return str;

    if(fromStart) {
        return '...' + str.slice(str.length - maxLength + 3);
    } else {
        return str.slice(0, maxLength - 3) + '...';
    }
}

function getStoredChats() {
    try {
        return JSON.parse(localStorage.savedAIChats).chats;
    } catch {}

    return null;
}

if(!getStoredChats()) return;

const donateLink = `<p class="dsu-donate-link"><a href="https://liberapay.com/Haka/donate"><svg xmlns="http://www.w3.org/2000/svg" width="83" height="30"><rect id="back" fill="#f6c915" x="1" y=".5" width="82" height="29" rx="4"></rect><svg viewBox="0 0 80 80" height="16" width="16" x="7" y="7"><g transform="translate(-78.37-208.06)" fill="#1a171b"><path d="m104.28 271.1c-3.571 0-6.373-.466-8.41-1.396-2.037-.93-3.495-2.199-4.375-3.809-.88-1.609-1.308-3.457-1.282-5.544.025-2.086.313-4.311.868-6.675l9.579-40.05 11.69-1.81-10.484 43.44c-.202.905-.314 1.735-.339 2.489-.026.754.113 1.421.415 1.999.302.579.817 1.044 1.546 1.395.729.353 1.747.579 3.055.679l-2.263 9.278"></path><path d="m146.52 246.14c0 3.671-.604 7.03-1.811 10.07-1.207 3.043-2.879 5.669-5.01 7.881-2.138 2.213-4.702 3.935-7.693 5.167-2.992 1.231-6.248 1.848-9.767 1.848-1.71 0-3.42-.151-5.129-.453l-3.394 13.651h-11.162l12.52-52.19c2.01-.603 4.311-1.143 6.901-1.622 2.589-.477 5.393-.716 8.41-.716 2.815 0 5.242.428 7.278 1.282 2.037.855 3.708 2.024 5.02 3.507 1.307 1.484 2.274 3.219 2.904 5.205.627 1.987.942 4.11.942 6.373m-27.378 15.461c.854.202 1.91.302 3.167.302 1.961 0 3.746-.364 5.355-1.094 1.609-.728 2.979-1.747 4.111-3.055 1.131-1.307 2.01-2.877 2.64-4.714.628-1.835.943-3.858.943-6.071 0-2.161-.479-3.998-1.433-5.506-.956-1.508-2.615-2.263-4.978-2.263-1.61 0-3.118.151-4.525.453l-5.28 21.948"></path></g></svg><text fill="#1a171b" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" font-weight="700" font-size="14" x="50" y="20">Donate</text></svg></a></p>`;

const searchBarElem = document.createElement('input');
      searchBarElem.type = 'text';
      searchBarElem.placeholder = 'Search for messages...';
      searchBarElem.onchange = search;
      searchBarElem.name = 'dsu-search';

const containerElem = document.createElement('dialog');
      containerElem.id = 'DuckSearchUserscript';
      containerElem.innerHTML = `${donateLink} <div class="dsu-result-container"></div>`;
      containerElem.prepend(searchBarElem);
      containerElem.addEventListener('click', (event) => {
          if(event.target === containerElem) {
              containerElem.close();
          }
      });

const resultContainer = containerElem.querySelector('.dsu-result-container');
const openBtn = document.createElement('div');
      openBtn.id = 'dsu-open-btn';
      openBtn.innerText = 'Search...';
      openBtn.onclick = () => containerElem.showModal();

document.body.appendChild(containerElem);
document.body.appendChild(openBtn);

function getChatElemByTitle(title) {
    const divs = document.querySelectorAll('div[title]');

    for(const div of divs) {
        if(div.title.includes(title)) {
            return div;
        }
    }

    return null;
}

function getMessageElem(chatId, i = 0, isUser = false) {
    const index = isUser ? Math.floor(i / 2) : i;

    const id = `${chatId}-assistant-message-${index}-1`;
    const elem = document.querySelector(`section [id="${id}"]`);

    return isUser ? elem?.parentElement?.querySelector('div') : elem;
}

function createResultElem(result) {
    const { item, matches } = result;
    const { title, chatId } = item;
    const messages = item.messages;

    const resultElem = document.createElement('div');
          resultElem.classList.add('dsu-result');
          resultElem.innerHTML = `
              <div class="dsu-result-title"></div>
              <div class="dsu-result-match-container"></div>
          `;

    const resultTitleElem = resultElem.querySelector('.dsu-result-title');
    const matchContainerElem = resultElem.querySelector('.dsu-result-match-container');
    const truncatedTitle = truncate(item.title, 60).replaceAll('\n', ' ');

    resultTitleElem.innerText = `Chat | ${truncatedTitle}`;

    matches.forEach(match => {
        const { key, value, indices, refIndex } = match;
        const isTitle = key === 'title';
        const isUser = key === 'messages.content';
        const isAI = key === 'messages.parts.text';

        const tag = isTitle ? 'title' : 'message';

        const fancyTag = isTitle
          ? tag
          : `${tag}${isUser ? ' | You' : isAI ? ' | AI' : ''}`;

        const matchElem = document.createElement('div');
              matchElem.classList.add('dsu-match');
              matchElem.classList.add(tag);
              matchElem.innerHTML = `
                  <div class="dsu-match-tag"></div>
                  <div class="dsu-match-text"></div>
              `;

        // When match elem is clicked, open the chat and highlight the message the match is from
        matchElem.onclick = () => {
            containerElem.close();

            const chatElem = getChatElemByTitle(title);

            chatElem?.click();

            setTimeout(() => {
                const messageElem = getMessageElem(chatId, refIndex, isUser);

                messageElem.scrollIntoView({ behavior: 'smooth', block: 'center' });
                messageElem.classList.add('dsu-highlight');

                setTimeout(() => messageElem.classList.remove('dsu-highlight'), 5000);
            }, 250);
        };

        const matchTagElem = matchElem.querySelector('.dsu-match-tag');
        const matchTextElem = matchElem.querySelector('.dsu-match-text');
              matchTagElem.classList.add(tag);
              matchTagElem.textContent = fancyTag;

        let lastIndex = 0;

        for(const [start, end] of indices) {
            if(start > lastIndex) {
                let beforeText = value.slice(lastIndex, start);
                matchTextElem.appendChild(document.createTextNode(beforeText));
            }

            const mark = document.createElement('mark');
            mark.textContent = value.slice(start, end + 1);
            matchTextElem.appendChild(mark);

            lastIndex = end + 1;
        }

        if(lastIndex < value.length) {
            let afterText = value.slice(lastIndex);
            matchTextElem.appendChild(document.createTextNode(afterText));
        }


        matchContainerElem.appendChild(matchElem);
    });

    return resultElem;
}

function render(results) {
    resultContainer.innerHTML = '';

    results.forEach(x => {
        const elem = createResultElem(x);
        resultContainer.appendChild(elem);
    });

    if(!results || results?.length === 0) {
        const noResultsText = document.createElement('div');
              noResultsText.classList.add('dsu-no-match-text');
              noResultsText.innerText = 'The search yielded no results. (╥﹏╥)';

        resultContainer.appendChild(noResultsText);
    }
}

function search(e) {
    const query = e?.target?.value?.toLowerCase();

    if(query?.length === 0) {
        resultContainer.innerHTML = '';
        return;
    }

    const chats = getStoredChats();
    const results = new Fuse(chats, { ...fuseOptions, 'minMatchCharLength':  query.length })
                        .search(query);

    render(results);
}

GM_addStyle(`
#DuckSearchUserscript {
    height: fit-content;
    border: 1px solid #333333;
    width: 90%;
    max-width: 800px;
    max-height: 90%;
    background: #111;
    color: white;
    border-bottom-width: 5px;
    border-radius: 5px;
    box-shadow: 0px 0px 12px 0px #000000;
    position: relative;
}
#DuckSearchUserscript::backdrop {
    background: rgb(0 31 255 / 5%);
    backdrop-filter: blur(10px);
}
#DuckSearchUserscript input {
    font-size: 18px;
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
    border-radius: 5px;
}
.dsu-result {
    width: 100%;
    height: fit-content;
    padding: 10px 15px;
    border-bottom-width: 3px;
    box-sizing: border-box;
}
.dsu-result-match-container {
    background: #1c1c1c;
    box-shadow: inset 0px 0px 15px 8px #111;
    padding: 20px;
    border-radius: 3px;
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
}
.dsu-match {
    background: rgb(51 44 69);
    padding: 10px;
    border-radius: 5px;
    position: relative;
    padding-top: 21px;
    border: 1px solid grey;
    border-bottom-width: 3px;
    width: fit-content;
    box-shadow: inset 1px -3px 10px 0px black;
    transition: all 0.1s ease-in-out;
    cursor: pointer;
    user-select: none;
    width: 100%;
    box-sizing: border-box;
}
.dsu-result:first-of-type {
    margin-top: 20px;
}
.dsu-match.title {
    background: rgb(52 64 91);
}
.dsu-match-tag.title {
    background: #647ebb;
}
.dsu-match:hover {
    transform: scale(1.02);
}
.dsu-match:active {
    transform: scale(1);
}
.dsu-match-tag {
    text-transform: capitalize;
    font-weight: 600;
    position: absolute;
    top: -10px;
    left: -5px;
    background: #6c589f;
    border: 1px solid grey;
    border-radius: 15px;
    padding: 0 10px;
    box-shadow: inset 0px 5px 10px 0px black;
    border-top-width: 2px;
    white-space: nowrap;
}
.dsu-match-text {
    font-weight: 500;
    font-size: 16px;
}
.dsu-match-text mark {
    font-weight: 900;
}
.message .dsu-match-text mark {
    background-color: #d99dff;
}
.title .dsu-match-text mark {
    background-color: #96b1f1;
}
.dsu-result-container {
    max-height: 80vh;
    overflow: hidden;
    overflow-y: scroll;
    margin-top: 2px;
}
.dsu-result-title {
    font-weight: 700;
    font-size: 1.25em;
    border-bottom: 2px solid #313131;
    margin-bottom: 5px;
    border-radius: 3px;
    padding: 5px;
}
.dsu-no-match-text {
    padding: 30px;
    font-weight: 700;
    font-size: 16px;
    background: #1e0000;
    color: red;
    border: 1px solid #212121;
    border-radius: 5px;
}
#dsu-open-btn {
    width: fit-content;
    height: fit-content;
    position: absolute;
    top: 15px;
    left: 50%;
    transform: translateX(-50%);
    background: #1c1c1c;
    color: white;
    padding: 7px 70px;
    border-radius: 5px;
    border: 1px solid #2f2f2f;
    border-bottom-width: 3px;
    color: #6f6f6f;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}
#dsu-open-btn:hover {
    background: #1e1e1e;
}
.dsu-highlight {
    transition: all 0.5s ease;
    background-color: rgb(153 110 237 / 88%) !important;
    box-shadow: 0 0 20px 0px rgb(153 110 237);
    border-radius: 5px;
}
.dsu-donate-link {
    margin-top: 5px;
    position: absolute;
    right: 22px;
    top: 13px;
}
`);