Duck.AI 聊天搜索 🔎(DuckDuckGo 的 AI)

为 Duck.AI 聊天添加搜索栏,让你轻松搜索聊天中的消息内容。

  1. // ==UserScript==
  2. // @name:tr Duck.AI Sohbet Arama 🔎 (DuckDuckGo’nun Yapay Zekası)
  3. // @name:fr Recherche de Chat Duck.AI 🔎 (IA de DuckDuckGo)
  4. // @name:id Pencarian Chat Duck.AI 🔎 (AI dari DuckDuckGo)
  5. // @name:pt-BR Pesquisa de Chat Duck.AI 🔎 (IA do DuckDuckGo)
  6. // @name:es Búsqueda de Chat Duck.AI 🔎 (IA de DuckDuckGo)
  7. // @name:pl Wyszukiwarka Czatów Duck.AI 🔎 (AI DuckDuckGo)
  8. // @name:vi Tìm kiếm Chat Duck.AI 🔎 (AI của DuckDuckGo)
  9. // @name:uk Пошук чатів Duck.AI 🔎 (ШІ від DuckDuckGo)
  10. // @name:it Ricerca Chat Duck.AI 🔎 (IA di DuckDuckGo)
  11. // @name:nl Duck.AI Chatzoeker 🔎 (AI van DuckDuckGo)
  12. // @name:ru Поиск чатов Duck.AI 🔎 (ИИ от DuckDuckGo)
  13. // @name:ja Duck.AI チャット検索 🔎(DuckDuckGo の AI)
  14. // @name Duck.AI Chat Search 🔎 (DuckDuckGo's AI)
  15. // @name:ko Duck.AI 채팅 검색기 🔎 (DuckDuckGo의 AI)
  16. // @name:zh-CN Duck.AI 聊天搜索 🔎(DuckDuckGo 的 AI)
  17. // @name:de Duck.AI Chatsuche 🔎 (DuckDuckGo KI)
  18. // @description:it Aggiunge una barra di ricerca alla chat Duck.AI per trovare facilmente i messaggi nelle conversazioni.
  19. // @description:fr Ajoute une barre de recherche à Duck.AI pour rechercher facilement des messages dans vos discussions.
  20. // @description:pt-BR Adiciona uma barra de pesquisa ao Duck.AI para facilitar a busca de mensagens nas conversas.
  21. // @description:tr Duck.AI sohbetine bir arama çubuğu ekler; böylece önceki mesajlarınızı kolayca arayabilirsiniz.
  22. // @description:id Menambahkan bilah pencarian ke Duck.AI untuk memudahkan pencarian pesan dalam obrolan Anda.
  23. // @description:pl Dodaje pasek wyszukiwania do Duck.AI, umożliwiając łatwe wyszukiwanie wiadomości w czatach.
  24. // @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.
  25. // @description:nl Voegt een zoekbalk toe aan Duck.AI waarmee je eenvoudig berichten in je chats kunt zoeken.
  26. // @description Adds a chat search bar to Duck.AI so you can easily search messages in your conversations.
  27. // @description:es Añade una barra de búsqueda a Duck.AI para encontrar fácilmente mensajes en tus chats.
  28. // @description:ru Добавляет строку поиска в Duck.AI, чтобы вы могли легко находить сообщения в чатах.
  29. // @description:de Fügt Duck.AI eine Suchleiste hinzu, um Nachrichten in Chats einfach zu finden.
  30. // @description:ja Duck.AI に検索バーを追加し、チャット内のメッセージを簡単に検索できるようにします。
  31. // @description:uk Додає панель пошуку в Duck.AI, щоб легко знаходити повідомлення у чатах.
  32. // @description:ko Duck.AI 채팅에 검색창을 추가하여 이전 메시지를 쉽게 찾을 수 있습니다.
  33. // @description:zh-CN 为 Duck.AI 聊天添加搜索栏,让你轻松搜索聊天中的消息内容。
  34. // @require https://cdn.jsdelivr.net/npm/fuse.js@7.1.0
  35. // @supportURL https://github.com/Hakorr/Userscripts
  36. // @match https://duckduckgo.com/*duckai*
  37. // @run-at document-load
  38. // @grant GM_addStyle
  39. // @namespace HKR
  40. // @author HKR
  41. // @version 1.1
  42. // ==/UserScript==
  43.  
  44. const fuseOptions = {
  45. includeScore: true,
  46. includeMatches: true,
  47. threshold: 0.2,
  48. ignoreLocation: true,
  49. distance: Infinity,
  50. keys: [
  51. 'title',
  52. 'messages.content',
  53. 'messages.parts.text'
  54. ],
  55. };
  56.  
  57. function truncate(str, maxLength, fromStart = false) {
  58. if(str.length <= maxLength) return str;
  59.  
  60. if(fromStart) {
  61. return '...' + str.slice(str.length - maxLength + 3);
  62. } else {
  63. return str.slice(0, maxLength - 3) + '...';
  64. }
  65. }
  66.  
  67. function getStoredChats() {
  68. try {
  69. return JSON.parse(localStorage.savedAIChats).chats;
  70. } catch {}
  71.  
  72. return null;
  73. }
  74.  
  75. if(!getStoredChats()) return;
  76.  
  77. 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>`;
  78.  
  79. const searchBarElem = document.createElement('input');
  80. searchBarElem.type = 'text';
  81. searchBarElem.placeholder = 'Search for messages...';
  82. searchBarElem.onchange = search;
  83. searchBarElem.name = 'dsu-search';
  84.  
  85. const containerElem = document.createElement('dialog');
  86. containerElem.id = 'DuckSearchUserscript';
  87. containerElem.innerHTML = `${donateLink} <div class="dsu-result-container"></div>`;
  88. containerElem.prepend(searchBarElem);
  89. containerElem.addEventListener('click', (event) => {
  90. if(event.target === containerElem) {
  91. containerElem.close();
  92. }
  93. });
  94.  
  95. const resultContainer = containerElem.querySelector('.dsu-result-container');
  96. const openBtn = document.createElement('div');
  97. openBtn.id = 'dsu-open-btn';
  98. openBtn.innerText = 'Search...';
  99. openBtn.onclick = () => containerElem.showModal();
  100.  
  101. document.body.appendChild(containerElem);
  102. document.body.appendChild(openBtn);
  103.  
  104. function getChatElemByTitle(title) {
  105. const divs = document.querySelectorAll('div[title]');
  106.  
  107. for(const div of divs) {
  108. if(div.title.includes(title)) {
  109. return div;
  110. }
  111. }
  112.  
  113. return null;
  114. }
  115.  
  116. function getMessageElem(chatId, i = 0, isUser = false) {
  117. const index = isUser ? Math.floor(i / 2) : i;
  118.  
  119. const id = `${chatId}-assistant-message-${index}-1`;
  120. const elem = document.querySelector(`section [id="${id}"]`);
  121.  
  122. return isUser ? elem?.parentElement?.querySelector('div') : elem;
  123. }
  124.  
  125. function createResultElem(result) {
  126. const { item, matches } = result;
  127. const { title, chatId } = item;
  128. const messages = item.messages;
  129.  
  130. const resultElem = document.createElement('div');
  131. resultElem.classList.add('dsu-result');
  132. resultElem.innerHTML = `
  133. <div class="dsu-result-title"></div>
  134. <div class="dsu-result-match-container"></div>
  135. `;
  136.  
  137. const resultTitleElem = resultElem.querySelector('.dsu-result-title');
  138. const matchContainerElem = resultElem.querySelector('.dsu-result-match-container');
  139. const truncatedTitle = truncate(item.title, 60).replaceAll('\n', ' ');
  140.  
  141. resultTitleElem.innerText = `Chat | ${truncatedTitle}`;
  142.  
  143. matches.forEach(match => {
  144. const { key, value, indices, refIndex } = match;
  145. const isTitle = key === 'title';
  146. const isUser = key === 'messages.content';
  147. const isAI = key === 'messages.parts.text';
  148.  
  149. const tag = isTitle ? 'title' : 'message';
  150.  
  151. const fancyTag = isTitle
  152. ? tag
  153. : `${tag}${isUser ? ' | You' : isAI ? ' | AI' : ''}`;
  154.  
  155. const matchElem = document.createElement('div');
  156. matchElem.classList.add('dsu-match');
  157. matchElem.classList.add(tag);
  158. matchElem.innerHTML = `
  159. <div class="dsu-match-tag"></div>
  160. <div class="dsu-match-text"></div>
  161. `;
  162.  
  163. // When match elem is clicked, open the chat and highlight the message the match is from
  164. matchElem.onclick = () => {
  165. containerElem.close();
  166.  
  167. const chatElem = getChatElemByTitle(title);
  168.  
  169. chatElem?.click();
  170.  
  171. setTimeout(() => {
  172. const messageElem = getMessageElem(chatId, refIndex, isUser);
  173.  
  174. messageElem.scrollIntoView({ behavior: 'smooth', block: 'center' });
  175. messageElem.classList.add('dsu-highlight');
  176.  
  177. setTimeout(() => messageElem.classList.remove('dsu-highlight'), 5000);
  178. }, 250);
  179. };
  180.  
  181. const matchTagElem = matchElem.querySelector('.dsu-match-tag');
  182. const matchTextElem = matchElem.querySelector('.dsu-match-text');
  183. matchTagElem.classList.add(tag);
  184. matchTagElem.textContent = fancyTag;
  185.  
  186. let lastIndex = 0;
  187.  
  188. for(const [start, end] of indices) {
  189. if(start > lastIndex) {
  190. let beforeText = value.slice(lastIndex, start);
  191. matchTextElem.appendChild(document.createTextNode(beforeText));
  192. }
  193.  
  194. const mark = document.createElement('mark');
  195. mark.textContent = value.slice(start, end + 1);
  196. matchTextElem.appendChild(mark);
  197.  
  198. lastIndex = end + 1;
  199. }
  200.  
  201. if(lastIndex < value.length) {
  202. let afterText = value.slice(lastIndex);
  203. matchTextElem.appendChild(document.createTextNode(afterText));
  204. }
  205.  
  206.  
  207. matchContainerElem.appendChild(matchElem);
  208. });
  209.  
  210. return resultElem;
  211. }
  212.  
  213. function render(results) {
  214. resultContainer.innerHTML = '';
  215.  
  216. results.forEach(x => {
  217. const elem = createResultElem(x);
  218. resultContainer.appendChild(elem);
  219. });
  220.  
  221. if(!results || results?.length === 0) {
  222. const noResultsText = document.createElement('div');
  223. noResultsText.classList.add('dsu-no-match-text');
  224. noResultsText.innerText = 'The search yielded no results. (╥﹏╥)';
  225.  
  226. resultContainer.appendChild(noResultsText);
  227. }
  228. }
  229.  
  230. function search(e) {
  231. const query = e?.target?.value?.toLowerCase();
  232.  
  233. if(query?.length === 0) {
  234. resultContainer.innerHTML = '';
  235. return;
  236. }
  237.  
  238. const chats = getStoredChats();
  239. const results = new Fuse(chats, { ...fuseOptions, 'minMatchCharLength': query.length })
  240. .search(query);
  241.  
  242. render(results);
  243. }
  244.  
  245. GM_addStyle(`
  246. #DuckSearchUserscript {
  247. height: fit-content;
  248. border: 1px solid #333333;
  249. width: 90%;
  250. max-width: 800px;
  251. max-height: 90%;
  252. background: #111;
  253. color: white;
  254. border-bottom-width: 5px;
  255. border-radius: 5px;
  256. box-shadow: 0px 0px 12px 0px #000000;
  257. position: relative;
  258. }
  259. #DuckSearchUserscript::backdrop {
  260. background: rgb(0 31 255 / 5%);
  261. backdrop-filter: blur(10px);
  262. }
  263. #DuckSearchUserscript input {
  264. font-size: 18px;
  265. width: 100%;
  266. padding: 10px;
  267. box-sizing: border-box;
  268. border-radius: 5px;
  269. }
  270. .dsu-result {
  271. width: 100%;
  272. height: fit-content;
  273. padding: 10px 15px;
  274. border-bottom-width: 3px;
  275. box-sizing: border-box;
  276. }
  277. .dsu-result-match-container {
  278. background: #1c1c1c;
  279. box-shadow: inset 0px 0px 15px 8px #111;
  280. padding: 20px;
  281. border-radius: 3px;
  282. display: flex;
  283. gap: 20px;
  284. flex-wrap: wrap;
  285. }
  286. .dsu-match {
  287. background: rgb(51 44 69);
  288. padding: 10px;
  289. border-radius: 5px;
  290. position: relative;
  291. padding-top: 21px;
  292. border: 1px solid grey;
  293. border-bottom-width: 3px;
  294. width: fit-content;
  295. box-shadow: inset 1px -3px 10px 0px black;
  296. transition: all 0.1s ease-in-out;
  297. cursor: pointer;
  298. user-select: none;
  299. width: 100%;
  300. box-sizing: border-box;
  301. }
  302. .dsu-result:first-of-type {
  303. margin-top: 20px;
  304. }
  305. .dsu-match.title {
  306. background: rgb(52 64 91);
  307. }
  308. .dsu-match-tag.title {
  309. background: #647ebb;
  310. }
  311. .dsu-match:hover {
  312. transform: scale(1.02);
  313. }
  314. .dsu-match:active {
  315. transform: scale(1);
  316. }
  317. .dsu-match-tag {
  318. text-transform: capitalize;
  319. font-weight: 600;
  320. position: absolute;
  321. top: -10px;
  322. left: -5px;
  323. background: #6c589f;
  324. border: 1px solid grey;
  325. border-radius: 15px;
  326. padding: 0 10px;
  327. box-shadow: inset 0px 5px 10px 0px black;
  328. border-top-width: 2px;
  329. white-space: nowrap;
  330. }
  331. .dsu-match-text {
  332. font-weight: 500;
  333. font-size: 16px;
  334. }
  335. .dsu-match-text mark {
  336. font-weight: 900;
  337. }
  338. .message .dsu-match-text mark {
  339. background-color: #d99dff;
  340. }
  341. .title .dsu-match-text mark {
  342. background-color: #96b1f1;
  343. }
  344. .dsu-result-container {
  345. max-height: 80vh;
  346. overflow: hidden;
  347. overflow-y: scroll;
  348. margin-top: 2px;
  349. }
  350. .dsu-result-title {
  351. font-weight: 700;
  352. font-size: 1.25em;
  353. border-bottom: 2px solid #313131;
  354. margin-bottom: 5px;
  355. border-radius: 3px;
  356. padding: 5px;
  357. }
  358. .dsu-no-match-text {
  359. padding: 30px;
  360. font-weight: 700;
  361. font-size: 16px;
  362. background: #1e0000;
  363. color: red;
  364. border: 1px solid #212121;
  365. border-radius: 5px;
  366. }
  367. #dsu-open-btn {
  368. width: fit-content;
  369. height: fit-content;
  370. position: absolute;
  371. top: 15px;
  372. left: 50%;
  373. transform: translateX(-50%);
  374. background: #1c1c1c;
  375. color: white;
  376. padding: 7px 70px;
  377. border-radius: 5px;
  378. border: 1px solid #2f2f2f;
  379. border-bottom-width: 3px;
  380. color: #6f6f6f;
  381. font-size: 15px;
  382. font-weight: 600;
  383. cursor: pointer;
  384. }
  385. #dsu-open-btn:hover {
  386. background: #1e1e1e;
  387. }
  388. .dsu-highlight {
  389. transition: all 0.5s ease;
  390. background-color: rgb(153 110 237 / 88%) !important;
  391. box-shadow: 0 0 20px 0px rgb(153 110 237);
  392. border-radius: 5px;
  393. }
  394. .dsu-donate-link {
  395. margin-top: 5px;
  396. position: absolute;
  397. right: 22px;
  398. top: 13px;
  399. }
  400. `);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址