// ==UserScript== // @name ChatGPT ChatTree 🌳 // @name:zh-CN ChatGPT ChatTree 🌳 // @namespace https://czz9.top // @version 2023.10.20.03 // @description ChatGPT ChatTree 🌳, 🚀permanent and unrestricted management of your interactions with ChatGPT🚀 🔄real-time updates and visualization of ChatGPT conversation tree🔄 💡ChatGPT conversation tips, custom annotations, bookmarks💡🔍Smart Search in ChatGPT: quickly locate specific conversations🔍 📋ChatGPT Interaction Management Panel, user-friendly interface, comprehensive ChatGPT interaction management options, categorization, tags, and more📋 // @description:ar ChatGPT ChatTree 🌳، 🚀إدارة دائمة وغير محدودة لتفاعلاتك مع ChatGPT🚀 🔄تحديث شجرة المحادثة ChatGPT بالوقت الفعلي + مرئيات🔄 💡نصائح للمحادثة مع ChatGPT، تعليقات مخصصة، إشارات مرجعية💡🔍 بحث ذكي في ChatGPT: تحديد المواضع الدقيقة للمحادثات بسرعة🔍 📋لوحة إدارة التفاعل مع ChatGPT، واجهة سهلة الاستخدام، خيارات إدارة التفاعل الكاملة مع ChatGPT، التصنيف، والعلامات وأكثر📋 // @description:bg ChatGPT ChatTree 🌳, 🚀постоянно и без ограничения управлявайте взаимодействията си с ChatGPT🚀 🔄реално време актуализации и визуализации на дървото на разговори с ChatGPT🔄 💡Съвети за разговори с ChatGPT, персонализирани коментари, отметки💡🔍 Интелигентно търсене в ChatGPT: бързо намиране на конкретни разговори🔍 📋Панел за управление на взаимодействията с ChatGPT, удобен интерфейс, пълни опции за управление на взаимодействията, категории, етикети и други📋 // @description:ckb ChatGPT ChatTree 🌳، 🚀بەردووم و بێ سنووریدانی کارپێکردنی تایبەتمەندیەکانت لەگەڵ ChatGPT🚀 🔄نوێکردنەوەی بەرزی ڕووداوی ChatGPT بە ڕاستی گشتی + ڕوانین🔄 💡پێشاندانی تایبەتمەندیەکانی ChatGPT، تێبینی تایبەت، نیشانکردن💡🔍 گەڕانی زانایی لە ChatGPT: چاودێری ئاسانی گفتوگۆی تایبەت🔍 📋پانێڵی باشکردنی تایبەتمەندی لەگەڵ ChatGPT، ڕووکاری ئاسان بەکارهێنان، هەڵبژاردنە پڕەکانی باشکردن، پۆلەکان، تاگ و زیاتر📋 // @description:cs ChatGPT ChatTree 🌳, 🚀trvalé a neomezené řízení interakcí s ChatGPT🚀 🔄aktualizace stromu konverzace ChatGPT v reálném čase + vizualizace🔄 💡tipy na konverzaci s ChatGPT, vlastní komentáře, záložky💡🔍 inteligentní vyhledávání v ChatGPT: rychlé nalezení konkrétních konverzací🔍 📋Panel pro správu interakcí s ChatGPT, uživatelsky přívětivé rozhraní, komplexní možnosti správy interakcí, kategorie, štítky a více📋 // @description:da ChatGPT ChatTree 🌳, 🚀permanent og ubegrænset styring af dine interaktioner med ChatGPT🚀 🔄real-tids opdatering og visualisering af ChatGPT samtaletræ🔄 💡tips til samtaler med ChatGPT, brugerdefinerede kommentarer, bogmærker💡🔍 Intelligent søgning i ChatGPT: hurtig lokalisering af specifikke samtaler🔍 📋Styringspanel for interaktion med ChatGPT, brugervenlig grænseflade, fuldstændige styringsmuligheder for interaktion, kategorier, tags og mere📋 // @description:de ChatGPT ChatTree 🌳, 🚀dauerhafte und uneingeschränkte Verwaltung Ihrer Interaktionen mit ChatGPT🚀 🔄Echtzeit-Aktualisierung und Visualisierung des ChatGPT-Gesprächsbaums🔄 💡Tipps für Gespräche mit ChatGPT, benutzerdefinierte Kommentare, Lesezeichen💡🔍Intelligente Suche in ChatGPT: schnelle Auffindung spezifischer Gespräche🔍 📋Verwaltungsoberfläche für Interaktionen mit ChatGPT, benutzerfreundlich, umfassende Verwaltungsoptionen, Kategorien, Tags und mehr📋 // @description:el ChatGPT ChatTree 🌳, 🚀μόνιμη και απεριόριστη διαχείριση των αλληλεπιδράσεών σας με το ChatGPT🚀 🔄πραγματικός χρόνος ενημέρωσης και οπτικοποίησης του δέντρου συνομιλίας ChatGPT🔄 💡συμβουλές για συνομιλίες με το ChatGPT, προσαρμοσμένα σχόλια, σελιδοδείκτες💡🔍Έξυπνη αναζήτηση στο ChatGPT: γρήγορος εντοπισμός συγκεκριμένων συνομιλιών🔍 📋Πίνακας διαχείρισης αλληλεπιδράσεων με το ChatGPT, φιλικό προς τον χρήστη περιβάλλον, πλήρεις επιλογές διαχείρισης, κατηγορίες, ετικέτες και περισσότερα📋 // @description:en ChatGPT ChatTree 🌳, 🚀permanent and unrestricted management of your interactions with ChatGPT🚀 🔄real-time updates and visualization of ChatGPT conversation tree🔄 💡ChatGPT conversation tips, custom annotations, bookmarks💡🔍Smart Search in ChatGPT: quickly locate specific conversations🔍 📋ChatGPT Interaction Management Panel, user-friendly interface, comprehensive ChatGPT interaction management options, categorization, tags, and more📋 // @description:eo ChatGPT ChatTree 🌳, 🚀eterna kaj senlima administrado de viaj interagoj kun ChatGPT🚀 🔄aktualigoj en reala tempo kaj vida prezento de la babilo-arbo de ChatGPT🔄 💡konsiletoj por babili kun ChatGPT, propraj notoj, legosignoj💡🔍Inteligenta serĉo en ChatGPT: rapide trovi specifajn konversaciojn🔍 📋Administra panelo por interagoj kun ChatGPT, uzantamika interfaco, ampleksaj opcioj por administri interagojn, kategorioj, etikedoj kaj pli📋 // @description:es ChatGPT ChatTree 🌳, 🚀gestión permanente e ilimitada de tus interacciones con ChatGPT🚀 🔄actualizaciones en tiempo real y visualización del árbol de conversación de ChatGPT🔄 💡consejos para conversaciones con ChatGPT, anotaciones personalizadas, marcadores💡🔍Búsqueda inteligente en ChatGPT: localiza rápidamente conversaciones específicas🔍 📋Panel de gestión de interacción con ChatGPT, interfaz fácil de usar, opciones completas de gestión de interacción, categorización, etiquetas y más📋 // @description:fi ChatGPT ChatTree 🌳, 🚀jatkuva ja rajoittamaton vuorovaikutuksesi hallinta ChatGPT:n kanssa🚀 🔄ChatGPT-keskustelupuun reaaliaikaiset päivitykset ja visualisointi🔄 💡vinkkejä keskusteluihin ChatGPT:n kanssa, mukautetut huomautukset, kirjanmerkit💡🔍Älykäs haku ChatGPT:ssa: löydä nopeasti tiettyjä keskusteluja🔍 📋ChatGPT-vuorovaikutuksen hallintapaneeli, käyttäjäystävällinen käyttöliittymä, kattavat vuorovaikutuksen hallintaoptiot, luokittelu, tagit ja lisää📋 // @description:fr ChatGPT ChatTree 🌳, 🚀surveillance continue et sans restriction de vos dialogues avec ChatGPT🚀 🔄rafraîchissement et affichage en direct de l'arborescence des échanges avec ChatGPT🔄 💡astuces pour discuter avec ChatGPT, notes sur mesure, favoris💡🔍Fouille astucieuse dans ChatGPT : repérage immédiat des discussions ciblées🔍 📋Tableau de bord pour la gestion des interactions avec ChatGPT, ergonomie intuitive, choix exhaustifs pour la supervision des dialogues, classement, balises et davantage📋 // @description:fr-CA ChatGPT ChatTree 🌳, 🚀contrôle ininterrompu de vos échanges avec ChatGPT🚀 🔄actualisation en temps réel de la structure de conversation de ChatGPT🔄 💡trucs pour converser avec ChatGPT, commentaires personnalisés, marque-pages💡🔍identification express des conversations spécifiques🔍 📋Tableau de contrôle des interactions avec ChatGPT, interface accessible, catégorisation, balises et plus📋 // @description:he ChatGPT ChatTree 🌳, 🚀ניהול תמידי וללא הגבלות של האינטראקציה שלך עם ChatGPT🚀 🔄עדכונים בזמן אמת וויזואליזציה של עץ השיחה של ChatGPT🔄 💡טיפים לשיחה עם ChatGPT, הערות מותאמות אישית, סימניות💡🔍חיפוש חכם ב-ChatGPT: מיקום מהיר של שיחות מסוימות🔍 📋פנל ניהול האינטראקציה עם ChatGPT, ממשק ידידותי למשתמש, אפשרויות ניהול האינטראקציה המלאות, קטגוריזציה, תגיות ועוד📋 // @description:hr ChatGPT ChatTree 🌳, 🚀stalno i neograničeno upravljanje vašim interakcijama s ChatGPT-om🚀 🔄ažuriranja u stvarnom vremenu i vizualizacija ChatGPT-ovog razgovornog stabla🔄 💡savjeti za razgovore s ChatGPT-om, prilagođene napomene, oznake💡🔍Pametno pretraživanje u ChatGPT: brzo lociranje određenih razgovora🔍 📋Ploča za upravljanje interakcijama s ChatGPT-om, sučelje pogodno za korisnika, sveobuhvatne opcije za upravljanje interakcijama, kategorizacija, oznake i još mnogo toga📋 // @description:hu ChatGPT ChatTree 🌳, 🚀állandó és korlátlan interakciók kezelése a ChatGPT-vel🚀 🔄ChatGPT beszélgetési fa valós idejű frissítései és vizualizációja🔄 💡beszélgetési tippek a ChatGPT-tel, egyéni megjegyzések, könyvjelzők💡🔍Intelligens keresés a ChatGPT-ben: gyorsan megtalálja a kijelölt beszélgetéseket🔍 📋ChatGPT Interakció Kezelési Panel, felhasználóbarát felület, átfogó interakciókezelési lehetőségek, kategorizálás, címkék és több📋 // @description:id ChatGPT ChatTree 🌳, 🚀pengelolaan interaksi Anda dengan ChatGPT secara permanen dan tanpa batas🚀 🔄pembaruan real-time dan visualisasi pohon percakapan ChatGPT🔄 💡tips percakapan ChatGPT, anotasi kustom, bookmark💡🔍Pencarian Cerdas di ChatGPT: cepat menemukan percakapan spesifik🔍 📋Panel Manajemen Interaksi ChatGPT, antarmuka yang ramah pengguna, pilihan manajemen interaksi ChatGPT yang komprehensif, kategorisasi, tag, dan lain-lain📋 // @description:it ChatGPT ChatTree 🌳, 🚀gestione permanente e illimitata delle tue interazioni con ChatGPT🚀 🔄aggiornamenti in tempo reale e visualizzazione dell'albero di conversazione di ChatGPT🔄 💡consigli per conversazioni con ChatGPT, annotazioni personalizzate, segnalibri💡🔍Ricerca intelligente in ChatGPT: individuazione rapida di conversazioni specifiche🔍 📋Pannello di gestione interazioni con ChatGPT, interfaccia user-friendly, opzioni complete di gestione interazioni, categorizzazione, tag e altro ancora📋 // @description:ja ChatGPT ChatTree 🌳, 🚀ChatGPTとの対話を永続的かつ無制限に管理🚀 🔄ChatGPT会話ツリーのリアルタイム更新と可視化🔄 💡ChatGPTとの会話のヒント、カスタム注釈、ブックマーク💡🔍ChatGPTでのスマート検索:特定の会話を迅速に見つける🔍 📋ChatGPTとの対話管理パネル、ユーザーフレンドリーなインターフェース、対話管理の包括的なオプション、カテゴリー、タグなど📋 // @description:ka ChatGPT ChatTree 🌳, 🚀თქვენი ინტერაქციების მუდმივი და ულიმიტო მართვა ChatGPT-თან🚀 🔄ChatGPT სტუმრობის ხეს რეალურ დროში განახლებები და ვიზუალიზაცია🔄 💡რჩევები სტუმრობაში ChatGPT-თან, პერსონალური კომენტარები, წიგნის ნიშნები💡🔍სწრაფი ძებნა ChatGPT-ში: კონკრეტულ სასტუმროში სწრაფი მოძებნა🔍 📋ChatGPT-თან ინტერაქციის მართვის პანელი, მომხმარებლისთვის მეგობრიანი ინტერფეისი, ინტერაქციის მართვის რეგულაცია, კატეგორიზაცია, თეგები და სხვა📋 // @description:ko ChatGPT ChatTree 🌳, 🚀ChatGPT와의 상호작용을 영구적이고 무제한으로 관리🚀 🔄ChatGPT 대화 트리의 실시간 업데이트 및 시각화🔄 💡ChatGPT 대화 팁, 사용자 정의 주석, 북마크💡🔍ChatGPT 스마트 검색: 특정 대화를 빠르게 찾기🔍 📋ChatGPT 상호작용 관리 패널, 사용자 친화적 인터페이스, 전체 상호 작용 관리 옵션, 카테고리, 태그 및 기타📋 // @description:nb ChatGPT ChatTree 🌳, 🚀permanent og ubegrenset styring av dine interaksjoner med ChatGPT🚀 🔄sanntidsoppdateringer og visualisering av ChatGPT samtaletre🔄 💡ChatGPT samtale tips, egendefinerte kommentarer, bokmerker💡🔍Smart søk i ChatGPT: raskt finne spesifikke samtaler🔍 📋ChatGPT interaksjonsstyringspanel, brukervennlig grensesnitt, omfattende interaksjonsstyringsalternativer, kategorisering, tagger og mer📋 // @description:nl ChatGPT ChatTree 🌳, 🚀permanente en onbeperkte beheer van uw interacties met ChatGPT🚀 🔄real-time updates en visualisatie van ChatGPT gespreksboom🔄 💡ChatGPT gesprekstips, aangepaste opmerkingen, bladwijzers💡🔍Slim zoeken in ChatGPT: snel specifieke gesprekken lokaliseren🔍 📋ChatGPT Interactiebeheerpaneel, gebruiksvriendelijke interface, uitgebreide beheeropties voor interactie, categorisatie, tags en meer📋 // @description:pl ChatGPT ChatTree 🌳, 🚀stałe i nieograniczone zarządzanie interakcjami z ChatGPT🚀 🔄aktualizacje w czasie rzeczywistym i wizualizacja drzewa rozmów z ChatGPT🔄 💡wskazówki do rozmów z ChatGPT, niestandardowe komentarze, zakładki💡🔍inteligentne wyszukiwanie w ChatGPT: szybkie lokalizowanie konkretnych rozmów🔍 📋Panel zarządzania interakcjami z ChatGPT, intuicyjny interfejs, kompleksowe opcje zarządzania interakcjami, kategorie, etykiety i więcej📋 // @description:pt-BR ChatGPT ChatTree 🌳, 🚀gerenciamento permanente e irrestrito de suas interações com o ChatGPT🚀 🔄atualizações em tempo real e visualização da árvore de conversas do ChatGPT🔄 💡dicas de conversa com ChatGPT, anotações personalizadas, favoritos💡🔍Busca inteligente no ChatGPT: localizar rapidamente conversas específicas🔍 📋Painel de gerenciamento de interação com ChatGPT, interface amigável, opções abrangentes de gerenciamento de interação, categorização, tags e mais📋 // @description:ro ChatGPT ChatTree 🌳, 🚀administrare permanentă și nelimitată a interacțiunilor tale cu ChatGPT🚀 🔄actualizări în timp real și vizualizarea arborelui de conversație ChatGPT🔄 💡sfaturi de conversație cu ChatGPT, comentarii personalizate, semne de carte💡🔍Căutare inteligentă în ChatGPT: localizare rapidă a conversațiilor specifice🔍 📋Panoul de administrare a interacțiunilor cu ChatGPT, interfață prietenoasă, opțiuni complete de administrare a interacțiunilor, categorii, etichete și mai mult📋 // @description:ru ChatGPT ChatTree 🌳, 🚀постоянное и неограниченное управление вашими взаимодействиями с ChatGPT🚀 🔄обновления в реальном времени и визуализация дерева диалогов ChatGPT🔄 💡советы для разговоров с ChatGPT, настраиваемые комментарии, закладки💡🔍Умный поиск в ChatGPT: быстрое нахождение конкретных диалогов🔍 📋Панель управления взаимодействием с ChatGPT, удобный интерфейс, полные настройки управления взаимодействием, категории, теги и многое другое📋 // @description:sk ChatGPT ChatTree 🌳, 🚀trvalé a neobmedzené riadenie vašich interakcií s ChatGPT🚀 🔄aktualizácie v reálnom čase a vizualizácia rozhovorového stromu ChatGPT🔄 💡tipy na rozhovory s ChatGPT, vlastné poznámky, záložky💡🔍inteligentné vyhľadávanie v ChatGPT: rýchle nájdenie konkrétnych rozhovorov🔍 📋Panel na správu interakcií s ChatGPT, používateľsky prívetivé rozhranie, komplexné možnosti riadenia interakcií, kategórie, štítky a viac📋 // @description:sr ChatGPT ChatTree 🌳, 🚀stalno i neograničeno upravljanje vašim interakcijama sa ChatGPT🚀 🔄ažuriranja u realnom vremenu i vizualizacija stabla razgovora ChatGPT🔄 💡saveti za razgovore sa ChatGPT, prilagođene napomene, obeleživači💡🔍Pametna pretraga u ChatGPT: brzo lociranje specifičnih razgovora🔍 📋Panel za upravljanje interakcijama sa ChatGPT, korisnički prijatan interfejs, sveobuhvatne opcije za upravljanje interakcijama, kategorizacija, oznake i više📋 // @description:sv ChatGPT ChatTree 🌳, 🚀permanent och obegränsad hantering av dina interaktioner med ChatGPT🚀 🔄real-tidsuppdateringar och visualisering av ChatGPTs konversationsträd🔄 💡ChatGPT konversationstips, anpassade anteckningar, bokmärken💡🔍Smart sökning i ChatGPT: snabbt lokalisera specifika konversationer🔍 📋ChatGPT Interaktionshanteringspanel, användarvänligt gränssnitt, omfattande interaktionshanteringsalternativ, kategorisering, taggar och mer📋 // @description:th ChatGPT ChatTree 🌳, 🚀การจัดการที่ถาวรและไม่จำกัดกับการโต้ตอบของคุณกับ ChatGPT🚀 🔄การอัปเดตและการแสดงภาพของต้นไม้บทสนทนาของ ChatGPT ในเวลาจริง🔄 💡เคล็ดลับการสนทนา ChatGPT, หมายเหตุที่กำหนดเอง, บุ๊กมาร์ก💡🔍การค้นหาอย่างฉลาดใน ChatGPT: ค้นหาบทสนทนาเฉพาะอย่างรวดเร็ว🔍 📋แผงควบคุมการโต้ตอบกับ ChatGPT, อินเตอร์เฟซที่เป็นมิตร, ตัวเลือกการจัดการโต้ตอบที่ครอบคลุม, การจัดประเภท, แท็ก และอื่น ๆ📋 // @description:tr ChatGPT ChatTree 🌳, 🚀ChatGPT ile etkileşimlerinizi kalıcı ve sınırsız bir şekilde yönetme🚀 🔄ChatGPT konuşma ağacının gerçek zamanlı güncellemeleri ve görselleştirmesi🔄 💡ChatGPT ile konuşma ipuçları, özel notlar, yer imleri💡🔍ChatGPT'te Akıllı Arama: belirli konuşmaları hızlı bir şekilde bulun🔍 📋ChatGPT Etkileşim Yönetim Paneli, kullanıcı dostu arayüz, kapsamlı etkileşim yönetim seçenekleri, kategorizasyon, etiketler ve daha fazlası📋 // @description:uk ChatGPT ChatTree 🌳, 🚀постійне та безобмежене управління вашими взаємодіями з ChatGPT🚀 🔄оновлення дерева розмови ChatGPT у реальному часі + візуалізація🔄 💡поради для розмови з ChatGPT, власні анотації, закладки💡🔍розумний пошук в ChatGPT: швидке знаходження конкретних розмов🔍 📋Панель управління взаємодіями з ChatGPT, зручний інтерфейс, повний набір опцій для управління взаємодіями, категорії, теги та більше📋 // @description:ug ChatGPT ChatTree 🌳, 🚀مەڭگۈلۈك ۋە چەكسىز بولغان ChatGPT بىلەن ئالاقىڭىزنى باشقۇرۇڭ🚀 🔄ChatGPT سۆھبەت ئاگىدىنى چۈشەندۈرۈش ۋە كۆرسىتىش🔄 💡ChatGPT بىلەن سۆھبەتكە كېلىشىش ئۈچۈن تەۋسىيەلەر، شەخسىي ئىزاھاتلار، خەتكۈچلەر💡🔍ChatGPT دا ئەقىللىق ئىزدەش: مەلۇم سۆھبەتلەرنى تېز تاپالايدۇ🔍 📋ChatGPT ئارىلىق باشقۇرۇش تاختىسى، ئىشلىتىشكە يېڭىراك يۈزى، تولۇق باشقۇرۇش تاللانمىلىرى، تۈرلەر، خەتكۈچلەر ۋە باشقا📋 // @description:vi ChatGPT ChatTree 🌳, 🚀quản lý vĩnh viễn và không giới hạn các tương tác của bạn với ChatGPT🚀 🔄cập nhật và trực quan hóa cây trò chuyện ChatGPT theo thời gian thực🔄 💡mẹo trò chuyện với ChatGPT, ghi chú tùy chỉnh, dấu trang💡🔍Tìm kiếm thông minh trong ChatGPT: xác định nhanh các cuộc trò chuyện cụ thể🔍 📋Bảng điều khiển quản lý tương tác với ChatGPT, giao diện thân thiện, các tùy chọn quản lý tương tác đầy đủ, phân loại, thẻ và nhiều hơn nữa📋 // @description:zh-CN ChatGPT ChatTree 🌳,🚀永久、不受限制地管理您与ChatGPT的每一次互动🚀 🔄实时ChatGPT对话树更新+可视化🔄 💡ChatGPT对话提示,自定义注释,书签💡🔍 ChatGPT智能搜索:快速定位特定对话🔍 📋ChatGPT互动管理面板,界面友好,全面ChatGPT互动管理选项,分类、标签和更多📋 // @description:zh-TW ChatGPT ChatTree 🌳,🚀永久、不受限制地管理您與ChatGPT的每一次互動🚀 🔄即時ChatGPT對話樹更新+視覺化🔄 💡ChatGPT對話提示,自訂註解,書籤💡🔍 ChatGPT智能搜尋:快速定位特定對話🔍 📋ChatGPT互動管理面板,介面友善,全面ChatGPT互動管理選項,分類、標籤和更多📋 // @description:zh-HK ChatGPT ChatTree 🌳,🚀永久、不受限制地管理您與ChatGPT的每一次互動🚀 🔄實時ChatGPT對話樹更新+可視化🔄 💡ChatGPT對話提示,自定義註釋,書籤💡🔍 ChatGPT智能搜索:快速定位特定對話🔍 📋ChatGPT互動管理面板,界面友好,全面ChatGPT互動管理選項,分類、標籤和更多📋 // @description:zh-SG ChatGPT ChatTree 🌳,🚀永久、不受限制地管理您与ChatGPT的每一次互动🚀 🔄实时ChatGPT对话树更新+可视化🔄 💡ChatGPT对话提示,自定义注释,书签💡🔍 ChatGPT智能搜索:快速定位特定对话🔍 📋ChatGPT互动管理面板,界面友好,全面ChatGPT互动管理选项,分类、标签和更多📋 // @author cuizhenzhi // @match *://chat.openai.com/* // @grant GM_addStyle // @grant GM_getResourceText // @require https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js // @require https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/interact.js/1.10.11/interact.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js // @require https://code.jquery.com/jquery-3.5.1.min.js // @require https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js // @run-at document-end // @homepageURL https://github.com/cuizhenzhi/ChatTree // @supportURL https://github.com/cuizhenzhi/ChatTree/issues // @license GPL-2.0-only // @icon data:image/svg+xml;utf8, // @downloadURL none // ==/UserScript== (function () { "use strict"; const isDevelopmentMode = false; function log(...messages) { if (isDevelopmentMode) { console.log(...messages); } } function error(...messages) { console.error(...messages); } GM_addStyle(` #searchTopicContainer { margin-bottom: 20px; } .conversation { border: 1px solid #ccc; margin-bottom: 10px; padding: 10px; margin-top: 20px; margin-left: 20px; margin-right: 20px; display: flex; justify-content: space-between; align-items: center; transition: border 0.1s ease-in-out; /* 添加过渡效果 */ border-radius: 4px; /* 添加边框圆角 */ } .conversation:hover { border: 2px solid black; /* 鼠标悬停时改变边框样式 */ box-shadow: 0px 0px 10px black; /* 添加外边框阴影效果 */ } .category { background-color: #f3f3f3; display: inline-block; padding: 2px 8px; margin-right: 2px; margin-bottom: 2px; } .chatTreeTag { background-color: #e0e0e0; display: inline-block; padding: 2px 8px; margin-right: 5px; margin-bottom: 2px; } .delete-icon, .add-icon { cursor: pointer; margin-left: 5px; } .delete-icon:hover, .add-icon:hover { background: #adb5bd; } .conversation h3 { display: inline-block; margin: 0; /* 移除默认边距 */ } .conversation-actions { margin-left: auto; /* 推到右侧 */ } .topicContainer{ width: 35%; overflow: hidden; } .optionsContainer{ width: 12%; display: flex; justify-content: center; } .tagContainers { width: 26%; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; } .categoriesContainer { width: 26%; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; } #managePanel { color: black; overflow: hidden; background: rgb(255,253,249); position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 999; /* 根据需要设置,以确保该元素位于其他元素之上 */ } #panelToggleButtonSVGShow { position: fixed; left: 0; top: 40%; z-index: 1000; padding: 10px 20px; border: none; background: rgb(171, 104, 255); color: white; cursor: pointer; } .card-header{ height: 40px; display: flex; border: 1px solid #adb5bd; align-items: center; justify-content: space-between; padding: 0.5rem 1.25rem; /* Optional: for adding some space inside the card-header */ } .card-body{ height: 40px; display: flex; border: 1px solid #adb5bd; align-items: center; justify-content: space-between; padding: 0.5rem 1.25rem; /* Optional: for adding some space inside the card-header */ overflow: hidden; /* Add this line to hide the overflow content */ } .card-header h5 { margin: 0; /* Optional: to remove the default margin of the h5 element */ } .btn.btn-link:hover { cursor: pointer !important; text-decoration: underline !important; border: 1px solid #80bdff !important; border-radius: 5px !important; color: #005cbf !important; margin-right: auto; /* It pushes the link to the extreme right */ } .btn.btn-sm.btn-primary{ background: #007bff; color: white; border-radius: 5px !important; border: 1px solid #80bdff !important; height: 30px; cursor: pointer; } .btn.btn-sm.btn-danger{ background: #dc3545; color: white; border-radius: 5px !important; border: 1px solid red !important; height: 30px; cursor: pointer; } .btn.btn-sm.btn-success{ background: #28a745; color: white; border-radius: 5px !important; border: 1px solid green !important; height: 30px; cursor: pointer; } .collapse { height: 0; transition: height 0.5s ease-in-out; overflow: hidden; } @keyframes textBlink { 0%, 100% { color: black; } 50% { color: white; } } .blinkText { animation: textBlink 1s ease-in-out infinite; } @keyframes highlight { 0% { background-color: initial; } 50% { background-color: #ffffff; } 100% { background-color: initial; } } .highlight { animation: highlight 2s ease-in-out infinite; } @keyframes highlightt { 0% { background-color: initial; } 50% { background-color: #000000; } 100% { background-color: initial; } } .highlightt { animation: highlightt 2s ease-in-out infinite; } .close-button { margin-left: 5px; color: red; cursor: pointer; } .suggestions-container { position: absolute; left: 40px; width: 270px; top: 100%; /* Place it below the search box */ max-height: 150px; /* Adjust as per your preference */ overflow-y: auto; /* Add scrollbar if content is too much */ border: 1px solid #ccc; background-color: white; z-index: 99; /* Make it appear on top of other elements */ } .suggestion-item { padding: 10px; cursor: pointer; } .suggestion-item:hover { background-color: #eee; } .node-menu { display: none; position: absolute; border: 1px solid black; background-color: white; } #contentDiv { box-sizing: border-box; } /* Cursor styles */ .node { cursor: pointer; } .node circle { fill: #e5e5e5; } .node circle.chatgpt { } .node circle.用户 { fill: #1E90FF; } .node.descendant-dragging circle { fill: #b0e0e6 !important; } .node.selectedNode circle { fill: red !important; } .link.highlighted { /*stroke: yellow;*/ stroke: #808080; stroke-width: 4px; } .link.descendant-highlighted { stroke: #d3d3d3; /* 选择你想要的颜色 */ stroke-width: 4px; } .content-text { white-space: pre-line; } /* Menu styles */ .menu { opacity: 0; transition: opacity 300ms, transform 300ms; transform: translateX(100%); /* Start from the right */ border: none; /* Remove border */ } .menu.show { opacity: 1; transform: translateX(0); /* Move to its original position */ } #commentForm { width: 300px; padding: 20px; border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } #commentForm label, #commentForm textarea, #commentForm button { display: block; width: 100%; margin-bottom: 10px; } #commentForm textarea { resize: none; /* 禁止调整大小 */ } .commentHoverEffect { transition: all 0.3s ease-in-out; } .commentHoverEffect:hover { font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); } /*这是显示对话内容的注释样式*/ .comment-text { color: #ff5555; font-style: italic; margin-right: 10px; display: block; font-weight: bold; border-bottom: 2px dashed #ff5555; } /* SVG background */ #thumbnailSvg { /*background-color: rgba(128, 128, 128, 0.4); !* Gray with 0.4 opacity *!*/ /*background-image: linear-gradient(to top, #fad0c4 0%, #ffd1ff 100%);*/ background: linear-gradient(to top, rgba(250, 208, 196, 1) 0%, rgba(255, 209, 255, 0.9) 100%); /*background-image: linear-gradient(to top, rgba(220,220,220, 1) 0%, rgba(220,220,220, 1) 100%);*/ } #mainSvg { /*background-color: rgba(128, 128, 128, 0.4); !* Gray with 0.4 opacity *!*/ /*background-image: linear-gradient(to top, #fad0c4 0%, #ffd1ff 100%);*/ /*background-image: linear-gradient(to top, rgba(220,220,220, 0.2) 0%, rgba(220,220,220, 0.2) 100%);*/ } #search-container { top: 20px; left: 20px; display: flex; align-items: center; position: fixed; width: 50px; height: 40px; color: black; /*overflow: hidden;*/ background: transparent; /*flex-wrap: wrap;*/ /*z-index: 10000;*/ } #search-icon { width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; /* adjust as needed */ background: transparent; /*background-color: #f1f1f1;*/ } #search-box { margin-top: 10px; width: 300px; height: 40px; /*background-color: #f1f1f1;*/ border: none; padding: 0 10px; outline: none; background: transparent; opacity: 0; pointer-events: none; border-radius: 6px; } #search-btn { padding: 5px 15px; margin-left: 10px; display: none; } #search-history { position: absolute; top: 50px; left: 0; /*display: none;*/ opacity: 0; /* 设置为 0 使其默认隐藏 */ /*display: ;*/ display: flex; flex-direction: row; /* 横向排列 */ flex-wrap: wrap; /* 当空间不足时换行 */ width: 0px; min-height: 0px; /* instead of height: 0px; */ align-content: flex-start; height: 20px; max-height: 200px; /*align-items: center; !* 确保每行的内容垂直居中对齐 *!*/ overflow-y: scroll; overflow-x: hidden; padding: 10px; /* 为了避免内容紧贴边缘,给予一些内边距 */ /*z-index: 10001;*/ /*border-radius: 6px;*/ } #search-results-count { box-sizing: border-box; line-height: 26px; /* 重置行高 */ margin: 0; padding: 3px 6px; display: flex; justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ } .history-item { display: flex; align-items: center; width: auto; /* 根据内容自动调整宽度 */ max-width: 150px; /* 设置最大宽度 */ max-height: 30px; overflow: hidden; background-color: #f5f5f5; border-radius: 15px; padding: 5px 10px; margin-top: 10px; margin-right: 10px; /* 每个 history-item 之间的间距 */ /*gap: 10px;*/ } .history-text { /*max-width: 100px; !* 设置最大宽度 *!*/ flex-shrink: 1; min-width: 0; white-space: nowrap; /* 防止文本换行 */ overflow: hidden; text-overflow: ellipsis; /* 使用省略号 (...) 表示被裁切的文本 */ /*padding: 5px 10px;*/ } .history-delete { cursor: pointer; padding: 5px; border-radius: 50%; transition: background-color 0.3s; margin-left: auto; /* 这会将按钮推到其父元素的右边 */ } .history-delete:hover { background-color: rgba(200, 0, 0, 0.1); } #settingsDiv, .actionDiv, .rightMiddleDiv { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; position: fixed; z-index: 1000; background-color: rgba(255, 255, 255, 0.8); border-radius: 50%; /* 让div成为圆形 */ /*box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);*/ box-shadow: none; transition: background-color 0.3s; /* 平滑过渡效果 */ user-select: none; } #feedbackDiv { bottom: 40%; display: flex; right: 10px; cursor: pointer; color: deepskyblue; font-size: 1.5em; } #colorSelectDiv { bottom: 46%; right: 10px; cursor: pointer; color: deepskyblue; font-size: 1.5em; } .rightMiddleDiv:hover { background-color: rgba(200, 200, 255, 0.9); /* 悬停时的背景色 */ } #rightMiddleMenu { position: fixed; bottom: 52%; right: 10px; } .actionDiv { display: none; right: 10px; cursor: pointer; color: deepskyblue; font-size: 1.5em; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #settingsDiv { animation: spin 4s linear infinite; } #settingsDiv { right: 10px; bottom: 10px; color: rebeccapurple; /* 或你想要的任何颜色 */ cursor: pointer; display: flex; justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ } #plusDiv { bottom: 60px; right: 60px; } #minusDiv { bottom: 60px; } #thumbNailDiv { bottom: 10px; right: 60px; } #refreshTree { bottom: 10px; right: 110px; } #undoDiv { bottom: 60px; right: 160px; } #redoDiv { bottom: 60px; right: 110px; } #deleteDiv { bottom: 10px; right: 160px; } #settingsDiv:hover, .actionDiv:hover { background-color: rgba(200, 200, 255, 0.9); /* 悬停时的背景色 */ } `); const getLang = function () { let lang = `{ "ar": { "chatTreeRunning": "🥳🌳ChatTree🌳في التشغيل!🎉", "updateCurrentConversationTree": "🌈 تحديث شجرة المحادثة الحالية 🌈", "adjustBackgroundColorAndOpacity": "🎨 تعديل لون الخلفية والشفافية 🎨", "toggleConversationTree": "🌳 عرض/إخفاء شجرة المحادثة 🌳", "noDatabaseAndCreationFailed": "لا يوجد قاعدة بيانات، ولم يتم الإنشاء بنجاح! الخروج من البرنامج النصي!", "mismatchedLink": "الرابط غير متطابق، يرجى تحديث الصفحة!", "startUpdatingConversationTree": "بدء تحديث شجرة المحادثة", "selectedItem": "لقد اخترت {item}", "successSavingChanges": "تم حفظ التغييرات بنجاح!", "codeCopiedToClipboard": "تم نسخ الكود إلى الحافظة!", "contentCopied": "تم نسخ المحتوى!", "emptyCommentPrompt": "التعليق فارغ. هل تريد ضبط التعليق على \\"فارغ\\"؟", "confirmDeleteLinkData": "تم التحقق من الرابط كـ {item}! هل تريد حذف بيانات هذا الرابط؟ سيتم حذف جميع محتوى المحادثة والتعليقات!", "confirmCurrentURL": "تم التحقق من الرابط كـ {item}! هل أنت متأكد؟", "commentSetToEmpty": "تم تعيين التعليق على \\"فارغ\\"", "enterCommentFirst": "الرجاء إدخال التعليق أولا", "commentSaved": "تم حفظ التعليق", "conversationDataDeleted": "تم حذف بيانات المحادثة بنجاح", "executeSearchWithQuery": "تنفيذ البحث: {item}", "searchPlaceholder": "أدخل كلمات البحث...", "searchButton": "بحث", "searchInContent": "في محتوى المحادثة", "searchInComments": "في التعليقات", "searchInBoth": "في المحتوى والتعليقات", "goToPrevious": "الانتقال إلى السابق", "goToNext": "الانتقال إلى التالي", "numberOfMatches": "{matches} نتائج", "nodeDetails": "تفاصيل", "enterComment": "الرجاء إدخال تعليق", "userCommentSave": "حفظ", "userCommentCancel": "إلغاء", "userCommentClear": "مسح", "openAdminPanel": "لوحة الإدارة", "allCategoriesFilter": "جميع الفئات", "conversationTitle": "عنوان", "actionOptions": "الإجراءات", "conversationCategory": "الفئة", "conversationTags": "العلامات" }, "bg": { "chatTreeRunning": "🥳🌳ChatTree🌳работи!🎉", "updateCurrentConversationTree": "🌈 Актуализация на текущото дърво на разговори 🌈", "adjustBackgroundColorAndOpacity": "🎨 Регулиране на цвета на фона и прозрачността 🎨", "toggleConversationTree": "🌳 Показване/скриване на дървото на разговори 🌳", "noDatabaseAndCreationFailed": "Няма база данни и създаването не бе успешно! Скриптът излиза!", "mismatchedLink": "Линкът не съответства, моля, опреснете страницата!", "startUpdatingConversationTree": "Започнете актуализация на дървото на разговори", "selectedItem": "Избрахте {item}", "successSavingChanges": "Промените са запазени успешно!", "codeCopiedToClipboard": "Кодът е копиран в клипборда!", "contentCopied": "Съдържанието е копирано!", "emptyCommentPrompt": "Коментарът е празен. Искате ли да зададете коментар като \\"празен\\"?", "confirmDeleteLinkData": "Линкът е проверен като {item}! Сигурни ли сте, че искате да изтриете информацията за този линк? Всички съдържания на разговори и коментари ще бъдат изтрити!", "confirmCurrentURL": "Линкът е проверен като {item}! Сигурни ли сте?", "commentSetToEmpty": "Коментарът е зададен като \\"празен\\"", "enterCommentFirst": "Моля, първо въведете коментар", "commentSaved": "Коментарът е запазен", "conversationDataDeleted": "Данните за разговора са изтрити успешно", "executeSearchWithQuery": "Изпълнение на търсене: {item}", "searchPlaceholder": "Въведете ключови думи за търсене...", "searchButton": "Търсене", "searchInContent": "В съдържанието на разговора", "searchInComments": "В коментарите", "searchInBoth": "В съдържание и коментари", "goToPrevious": "Към предишния", "goToNext": "Към следващия", "numberOfMatches": "{matches} съвпадения", "nodeDetails": "Подробности", "enterComment": "Моля, въведете коментар", "userCommentSave": "Запази", "userCommentCancel": "Отказ", "userCommentClear": "Изчисти", "openAdminPanel": "Админ панел", "allCategoriesFilter": "Всички категории", "conversationTitle": "Заглавие", "actionOptions": "Действия", "conversationCategory": "Категория", "conversationTags": "Етикети" }, "cs": { "chatTreeRunning": "🥳🌳ChatTree🌳běží!🎉", "updateCurrentConversationTree": "🌈 Aktualizovat aktuální strom konverzace 🌈", "adjustBackgroundColorAndOpacity": "🎨 Upravit barvu pozadí a průhlednost 🎨", "toggleConversationTree": "🌳 Zobrazit/skrýt strom konverzace 🌳", "noDatabaseAndCreationFailed": "Není databáze a vytvoření se nezdařilo! Skript se ukončuje!", "mismatchedLink": "Odkaz nesouzní, obnovte stránku!", "startUpdatingConversationTree": "Začít aktualizovat strom konverzace", "selectedItem": "Vybrali jste {item}", "successSavingChanges": "Změny byly úspěšně uloženy!", "codeCopiedToClipboard": "Kód byl zkopírován do schránky!", "contentCopied": "Obsah byl zkopírován!", "emptyCommentPrompt": "Komentář je prázdný. Chcete nastavit komentář na \\"prázdný\\"?", "confirmDeleteLinkData": "Odkaz byl ověřen jako {item}! Opravdu chcete smazat informace tohoto odkazu? Veškerý obsah konverzace a komentáře budou smazány!", "confirmCurrentURL": "Odkaz byl ověřen jako {item}! Potvrzujete?", "commentSetToEmpty": "Komentář byl nastaven na \\"prázdný\\"", "enterCommentFirst": "Nejprve zadejte komentář", "commentSaved": "Komentář byl uložen", "conversationDataDeleted": "Data konverzace byla úspěšně smazána", "executeSearchWithQuery": "Spustit vyhledávání: {item}", "searchPlaceholder": "Zadejte klíčová slova pro vyhledávání...", "searchButton": "Hledat", "searchInContent": "Ve obsahu konverzace", "searchInComments": "V komentářích", "searchInBoth": "V obsahu i komentářích", "goToPrevious": "Jít na předchozí", "goToNext": "Jít na další", "numberOfMatches": "{matches} shod", "nodeDetails": "Detaily", "enterComment": "Zadejte komentář", "userCommentSave": "Uložit", "userCommentCancel": "Zrušit", "userCommentClear": "Vymazat", "openAdminPanel": "Administrační panel", "allCategoriesFilter": "Všechny kategorie", "conversationTitle": "Název", "actionOptions": "Akce", "conversationCategory": "Kategorie", "conversationTags": "Tagy" }, "da": { "chatTreeRunning": "🥳🌳ChatTree🌳 kører!🎉", "updateCurrentConversationTree": "🌈 Opdater nuværende samtaletræ 🌈", "adjustBackgroundColorAndOpacity": "🎨 Juster baggrundsfarve og gennemsigtighed 🎨", "toggleConversationTree": "🌳 Vis/skjul samtaletræ 🌳", "noDatabaseAndCreationFailed": "Ingen database, og oprettelsen mislykkedes! Scriptet afslutter!", "mismatchedLink": "Link matcher ikke, opdater siden!", "startUpdatingConversationTree": "Begynder at opdatere samtaletræ", "selectedItem": "Du valgte {item}", "successSavingChanges": "Ændringer gemt succesfuldt!", "codeCopiedToClipboard": "Kode kopieret til udklipsholder!", "contentCopied": "Indhold kopieret!", "emptyCommentPrompt": "Kommentaren er tom. Vil du sætte kommentaren til \\"tom\\"?", "confirmDeleteLinkData": "Linket er {item}! Er du sikker på, at du vil slette disse linkdata? Alle samtaler og kommentarer vil blive slettet!", "confirmCurrentURL": "Linket er {item}! Er du sikker?", "commentSetToEmpty": "Kommentar sat til \\"tom\\"", "enterCommentFirst": "Indtast kommentar først", "commentSaved": "Kommentar gemt", "conversationDataDeleted": "Samtaledata slettet succesfuldt", "executeSearchWithQuery": "Udfør søgning: {item}", "searchPlaceholder": "Indtast søgeord...", "searchButton": "Søg", "searchInContent": "I samtaleindholdet", "searchInComments": "I kommentarer", "searchInBoth": "Både i indhold og kommentarer", "goToPrevious": "Gå til den forrige", "goToNext": "Gå til den næste", "numberOfMatches": "{matches} match", "nodeDetails": "Detaljer", "enterComment": "Indtast kommentar", "userCommentSave": "Gem", "userCommentCancel": "Annuller", "userCommentClear": "Ryd", "openAdminPanel": "Admin panel", "allCategoriesFilter": "Alle kategorier", "conversationTitle": "Titel", "actionOptions": "Handlinger", "conversationCategory": "Kategori", "conversationTags": "Tags" }, "de": { "chatTreeRunning": "🥳🌳ChatTree🌳 läuft!🎉", "updateCurrentConversationTree": "🌈 Aktualisiere aktuellen Konversationsbaum 🌈", "adjustBackgroundColorAndOpacity": "🎨 Hintergrundfarbe und Transparenz anpassen 🎨", "toggleConversationTree": "🌳 Konversationsbaum anzeigen/verbergen 🌳", "noDatabaseAndCreationFailed": "Keine Datenbank und Erstellung fehlgeschlagen! Skript beendet!", "mismatchedLink": "Link stimmt nicht überein, bitte aktualisieren Sie die Seite!", "startUpdatingConversationTree": "Beginne mit der Aktualisierung des Konversationsbaums", "selectedItem": "Sie haben {item} ausgewählt", "successSavingChanges": "Änderungen erfolgreich gespeichert!", "codeCopiedToClipboard": "Code wurde in die Zwischenablage kopiert!", "contentCopied": "Inhalt kopiert!", "emptyCommentPrompt": "Der Kommentar ist leer. Möchten Sie den Kommentar auf \\"leer\\" setzen?", "confirmDeleteLinkData": "Der erkannte Link ist {item}! Möchten Sie wirklich die Informationen zu diesem Link löschen? Alle Gesprächsinhalte und Kommentare werden gelöscht!", "confirmCurrentURL": "Der erkannte Link ist {item}! Sicher?", "commentSetToEmpty": "Kommentar auf \\"leer\\" gesetzt", "enterCommentFirst": "Bitte geben Sie zuerst einen Kommentar ein", "commentSaved": "Kommentar gespeichert", "conversationDataDeleted": "Konversationsdaten erfolgreich gelöscht", "executeSearchWithQuery": "Suche ausführen: {item}", "searchPlaceholder": "Suchbegriffe eingeben...", "searchButton": "Suchen", "searchInContent": "Im Gesprächsinhalt", "searchInComments": "In den Kommentaren", "searchInBoth": "In Inhalt und Kommentaren", "goToPrevious": "Zum vorherigen gehen", "goToNext": "Zum nächsten gehen", "numberOfMatches": "{matches} Übereinstimmungen", "nodeDetails": "Details", "enterComment": "Bitte Kommentar eingeben", "userCommentSave": "Speichern", "userCommentCancel": "Abbrechen", "userCommentClear": "Löschen", "openAdminPanel": "Admin-Panel", "allCategoriesFilter": "Alle Kategorien", "conversationTitle": "Titel", "actionOptions": "Optionen", "conversationCategory": "Kategorie", "conversationTags": "Tags" }, "el": { "chatTreeRunning": "🥳🌳ChatTree🌳 Είναι σε λειτουργία!🎉", "updateCurrentConversationTree": "🌈 Ενημέρωση του τρέχοντος δέντρου συνομιλίας 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ρύθμιση του χρώματος φόντου και της διαφάνειας 🎨", "toggleConversationTree": "🌳 Εμφάνιση/Απόκρυψη δέντρου συνομιλίας 🌳", "noDatabaseAndCreationFailed": "Δεν υπάρχει βάση δεδομένων και η δημιουργία απέτυχε! Το σενάριο τερματίζεται!", "mismatchedLink": "Ο σύνδεσμος δεν ταιριάζει, παρακαλώ ανανεώστε τη σελίδα!", "startUpdatingConversationTree": "Ξεκινώντας την ενημέρωση του δέντρου συνομιλίας", "selectedItem": "Επιλέξατε {item}", "successSavingChanges": "Οι αλλαγές αποθηκεύτηκαν επιτυχώς!", "codeCopiedToClipboard": "Ο κώδικας αντιγράφηκε στο πρόχειρο!", "contentCopied": "Το περιεχόμενο αντιγράφηκε!", "emptyCommentPrompt": "Το σχόλιο είναι κενό. Θέλετε να ορίσετε το σχόλιο ως \\"κενό\\";", "confirmDeleteLinkData": "Ο σύνδεσμος που εντοπίστηκε είναι {item}! Είστε σίγουροι ότι θέλετε να διαγράψετε αυτά τα δεδομένα συνδέσμου; Όλο το περιεχόμενο της συζήτησης και τα σχόλια θα διαγραφούν!", "confirmCurrentURL": "Ο σύνδεσμος που εντοπίστηκε είναι {item}! Είστε σίγουροι;", "commentSetToEmpty": "Το σχόλιο ορίστηκε ως \\"κενό\\"", "enterCommentFirst": "Παρακαλώ εισάγετε πρώτα ένα σχόλιο", "commentSaved": "Το σχόλιο αποθηκεύτηκε", "conversationDataDeleted": "Τα δεδομένα συνομιλίας διαγράφηκαν επιτυχώς", "executeSearchWithQuery": "Εκτέλεση αναζήτησης: {item}", "searchPlaceholder": "Εισάγετε λέξεις κλειδιά αναζήτησης...", "searchButton": "Αναζήτηση", "searchInContent": "Στο περιεχόμενο της συνομιλίας", "searchInComments": "Στα σχόλια", "searchInBoth": "Τόσο στο περιεχόμενο όσο και στα σχόλια", "goToPrevious": "Πήγαινε στο προηγούμενο", "goToNext": "Πήγαινε στο επόμενο", "numberOfMatches": "{matches} αντιστοιχίες", "nodeDetails": "Λεπτομέρειες", "enterComment": "Πληκτρολογήστε σχόλιο", "userCommentSave": "Αποθήκευση", "userCommentCancel": "Ακύρωση", "userCommentClear": "Καθαρισμός", "openAdminPanel": "Πίνακας Διαχείρισης", "allCategoriesFilter": "Όλες οι Κατηγορίες", "conversationTitle": "Τίτλος", "actionOptions": "Ενέργειες", "conversationCategory": "Κατηγορία", "conversationTags": "Ετικέτες" }, "en": { "chatTreeRunning": "🥳🌳ChatTree🌳 is Running!🎉", "updateCurrentConversationTree": "🌈 Update Current Conversation Tree 🌈", "adjustBackgroundColorAndOpacity": "🎨 Adjust Background Color and Opacity 🎨", "toggleConversationTree": "🌳 Show/Hide Conversation Tree 🌳", "noDatabaseAndCreationFailed": "No database, and creation failed! The script is terminating.", "mismatchedLink": "Link does not match, please refresh the page!", "startUpdatingConversationTree": "Starting to update the conversation tree", "selectedItem": "You selected {item}", "successSavingChanges": "Changes Saved Successfully!", "codeCopiedToClipboard": "Code Copied to Clipboard!", "contentCopied": "Content Copied!", "emptyCommentPrompt": "The comment is empty. Do you want to set the comment to \\"empty\\"?", "confirmDeleteLinkData": "Detected link is {item}! Are you sure you want to delete this link's information? All conversation content and comments will be deleted!", "confirmCurrentURL": "Detected link is {item}! Are you sure?", "commentSetToEmpty": "Comment set to \\"empty\\"", "enterCommentFirst": "Please enter a comment first", "commentSaved": "Comment Saved", "conversationDataDeleted": "Conversation Data Successfully Deleted", "executeSearchWithQuery": "Executing Search: {item}", "searchPlaceholder": "Enter search keywords...", "searchButton": "Search", "searchInContent": "Conversation Content", "searchInComments": "User Comments", "searchInBoth": "Content & Comments", "goToPrevious": "Go to Previous", "goToNext": "Go to Next", "numberOfMatches": "{matches} matches found", "nodeDetails": "Details", "enterComment": "Please Enter Comment", "userCommentSave": "Save", "userCommentCancel": "Cancel", "userCommentClear": "Clear", "openAdminPanel": "Admin Panel", "allCategoriesFilter": "All Categories", "conversationTitle": "Title", "actionOptions": "Actions", "conversationCategory": "Category", "conversationTags": "Tags" }, "eo": { "chatTreeRunning": "🥳🌳ChatTree🌳 funkcias!🎉", "updateCurrentConversationTree": "🌈 Ĝisdatigi la aktivan konversacian arbon 🌈", "adjustBackgroundColorAndOpacity": "🎨 Agordi fondon koloron kaj opakecon 🎨", "toggleConversationTree": "🌳 Montri/Kaŝi konversacian arbon 🌳", "noDatabaseAndCreationFailed": "Neniu datumbazo, kaj ne sukcesis krei! Skripto eliras!", "mismatchedLink": "Ligilo ne kongruas, bonvolu refreŝigi la paĝon!", "startUpdatingConversationTree": "Komenci ĝisdatigi la konversacian arbon", "selectedItem": "Vi selektis {item}", "successSavingChanges": "Ŝanĝoj sukcese konservitaj!", "codeCopiedToClipboard": "Kodo estis kopii al tondejo!", "contentCopied": "Enhavo estis kopii!", "emptyCommentPrompt": "Komento estas malplena. Ĉu vi volas agordi komenton al \\"malplena\\"?", "confirmDeleteLinkData": "Detektis ligilon {item}! Ĉu vi certas, ke vi volas forigi ĉi tiun ligilon informon? Ĉiuj konversaciaj enhavoj kaj komentoj estos forigitaj!", "confirmCurrentURL": "Detektis ligilon {item}! Ĉu vi certas?", "commentSetToEmpty": "Komento estis agordita al \\"malplena\\"", "enterCommentFirst": "Bonvolu unue enigi komenton", "commentSaved": "Komento konservita", "conversationDataDeleted": "Konversaciaj datenoj sukcese forigitaj", "executeSearchWithQuery": "Efektivi serĉon: {item}", "searchPlaceholder": "Enmetu serĉajn ŝlosilvortojn...", "searchButton": "Serĉi", "searchInContent": "Konversacia enhavo", "searchInComments": "Komentoj de uzantoj", "searchInBoth": "Enhavo & Komentoj", "goToPrevious": "Iru al antaŭa", "goToNext": "Iru al sekvanta", "numberOfMatches": "{matches} kongruajn trovis", "nodeDetails": "Detaloj", "enterComment": "Bonvolu Enmeti Komenton", "userCommentSave": "Konservi", "userCommentCancel": "Nuligi", "userCommentClear": "Forigi", "openAdminPanel": "Malfermi Adminan Panelon", "allCategoriesFilter": "Ĉiuj Kategorioj", "conversationTitle": "Titolo", "actionOptions": "Agadoj", "conversationCategory": "Kategorio", "conversationTags": "Etikedoj" }, "es": { "chatTreeRunning": "🥳🌳¡ChatTree🌳 en funcionamiento!🎉", "updateCurrentConversationTree": "🌈 Actualizar el árbol de conversación actual 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajustar color de fondo y opacidad 🎨", "toggleConversationTree": "🌳 Mostrar/Ocultar árbol de conversación 🌳", "noDatabaseAndCreationFailed": "¡No hay base de datos y no se pudo crear! ¡Saliendo del script!", "mismatchedLink": "¡El enlace no coincide, por favor actualiza la página!", "startUpdatingConversationTree": "Comenzar a actualizar el árbol de conversación", "selectedItem": "Seleccionaste {item}", "successSavingChanges": "¡Cambios guardados con éxito!", "codeCopiedToClipboard": "¡Código copiado al portapapeles!", "contentCopied": "¡Contenido copiado!", "emptyCommentPrompt": "El comentario está vacío. ¿Quieres establecer el comentario como \\"vacío\\"?", "confirmDeleteLinkData": "¡Se detectó el enlace {item}! ¿Estás seguro de que quieres eliminar la información de este enlace? ¡Toda la conversación y los comentarios se eliminarán!", "confirmCurrentURL": "¡Se detectó el enlace {item}! ¿Estás seguro?", "commentSetToEmpty": "Comentario establecido como \\"vacío\\"", "enterCommentFirst": "Por favor, ingresa un comentario primero", "commentSaved": "Comentario guardado", "conversationDataDeleted": "Datos de la conversación eliminados con éxito", "executeSearchWithQuery": "Ejecutar búsqueda: {item}", "searchPlaceholder": "Ingrese palabras clave para buscar...", "searchButton": "Buscar", "searchInContent": "Contenido de la conversación", "searchInComments": "Comentarios de usuarios", "searchInBoth": "Contenido y comentarios", "goToPrevious": "Ir al anterior", "goToNext": "Ir al siguiente", "numberOfMatches": "{matches} coincidencias encontradas", "nodeDetails": "Detalles", "enterComment": "Por Favor Introduce un Comentario", "userCommentSave": "Guardar", "userCommentCancel": "Cancelar", "userCommentClear": "Borrar", "openAdminPanel": "Abrir Panel de Administración", "allCategoriesFilter": "Todas las Categorías", "conversationTitle": "Título", "actionOptions": "Opciones", "conversationCategory": "Categoría", "conversationTags": "Etiquetas" }, "fi": { "chatTreeRunning": "🥳🌳ChatTree🌳 on käynnissä!🎉", "updateCurrentConversationTree": "🌈 Päivitä nykyinen keskustelupuu 🌈", "adjustBackgroundColorAndOpacity": "🎨 Säädä taustavärin ja läpinäkyvyyden 🎨", "toggleConversationTree": "🌳 Näytä/Piilota keskustelupuu 🌳", "noDatabaseAndCreationFailed": "Tietokantaa ei ole, eikä luonti onnistunut! Skripti päättyy!", "mismatchedLink": "Linkki ei täsmää, päivitä sivu!", "startUpdatingConversationTree": "Aloita keskustelupuun päivittäminen", "selectedItem": "Valitsit kohteen {item}", "successSavingChanges": "Muutosten tallennus onnistui!", "codeCopiedToClipboard": "Koodi kopioitu leikepöydälle!", "contentCopied": "Sisältö kopioitu!", "emptyCommentPrompt": "Kommentti on tyhjä. Haluatko asettaa kommentin \\"tyhjäksi\\"?", "confirmDeleteLinkData": "Linkki {item} havaittu! Haluatko varmasti poistaa tämän linkin tiedot? Kaikki keskustelut ja kommentit poistetaan!", "confirmCurrentURL": "Linkki {item} havaittu! Oletko varma?", "commentSetToEmpty": "Kommentti asetettu \\"tyhjäksi\\"", "enterCommentFirst": "Syötä kommentti ensin", "commentSaved": "Kommentti tallennettu", "conversationDataDeleted": "Keskustelutiedot poistettu onnistuneesti", "executeSearchWithQuery": "Suorita haku: {item}", "searchPlaceholder": "Anna hakusanat...", "searchButton": "Hae", "searchInContent": "Keskustelun sisältö", "searchInComments": "Käyttäjien kommentit", "searchInBoth": "Sisältö ja kommentit", "goToPrevious": "Mene edelliseen", "goToNext": "Mene seuraavaan", "numberOfMatches": "{matches} osumaa löydetty", "nodeDetails": "Tiedot", "enterComment": "Ole Hyvä ja Kirjoita Kommentti", "userCommentSave": "Tallenna", "userCommentCancel": "Peruuta", "userCommentClear": "Tyhjennä", "openAdminPanel": "Avaa Hallintapaneeli", "allCategoriesFilter": "Kaikki Kategoriat", "conversationTitle": "Otsikko", "actionOptions": "Toiminnot", "conversationCategory": "Kategoria", "conversationTags": "Tagit" }, "fr": { "chatTreeRunning": "🥳🌳ChatTree🌳 est en cours d'exécution !🎉", "updateCurrentConversationTree": "🌈 Mettre à jour l'arbre de conversation actuel 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajuster la couleur de fond et l'opacité 🎨", "toggleConversationTree": "🌳 Afficher/Masquer l'arbre de conversation 🌳", "noDatabaseAndCreationFailed": "Pas de base de données et la création a échoué ! Sortie du script !", "mismatchedLink": "Le lien ne correspond pas, veuillez rafraîchir la page !", "startUpdatingConversationTree": "Commencer à mettre à jour l'arbre de conversation", "selectedItem": "Vous avez sélectionné {item}", "successSavingChanges": "Modifications enregistrées avec succès !", "codeCopiedToClipboard": "Le code a été copié dans le presse-papiers !", "contentCopied": "Contenu copié !", "emptyCommentPrompt": "Le commentaire est vide. Voulez-vous définir le commentaire comme \\"vide\\" ?", "confirmDeleteLinkData": "Le lien {item} a été détecté ! Êtes-vous sûr de vouloir supprimer les informations de ce lien ? Toutes les conversations et les commentaires seront supprimés !", "confirmCurrentURL": "Le lien {item} a été détecté ! Êtes-vous sûr ?", "commentSetToEmpty": "Commentaire défini comme \\"vide\\"", "enterCommentFirst": "Veuillez d'abord entrer un commentaire", "commentSaved": "Commentaire enregistré", "conversationDataDeleted": "Données de conversation supprimées avec succès", "executeSearchWithQuery": "Effectuer une recherche : {item}", "searchPlaceholder": "Entrez des mots-clés pour rechercher...", "searchButton": "Rechercher", "searchInContent": "Contenu de la conversation", "searchInComments": "Commentaires des utilisateurs", "searchInBoth": "Contenu et commentaires", "goToPrevious": "Aller au précédent", "goToNext": "Aller au suivant", "numberOfMatches": "{matches} correspondances trouvées", "nodeDetails": "Détails", "enterComment": "Veuillez Entrer un Commentaire", "userCommentSave": "Enregistrer", "userCommentCancel": "Annuler", "userCommentClear": "Effacer", "openAdminPanel": "Ouvrir le Panneau d'Administration", "allCategoriesFilter": "Toutes les Catégories", "conversationTitle": "Titre", "actionOptions": "Options", "conversationCategory": "Catégorie", "conversationTags": "Étiquettes" }, "fr-CA": { "chatTreeRunning": "🥳🌳ChatTree🌳 est en marche !🎉", "updateCurrentConversationTree": "🌈 Mettre à jour l'arbre de conversation actuel 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajustez la couleur de fond et l'opacité 🎨", "toggleConversationTree": "🌳 Afficher/Masquer l'arbre de conversation 🌳", "noDatabaseAndCreationFailed": "Pas de base de données et la création a échoué ! Le script se termine !", "mismatchedLink": "Le lien ne correspond pas, veuillez rafraîchir la page !", "startUpdatingConversationTree": "Commencer la mise à jour de l'arbre de conversation", "selectedItem": "Vous avez sélectionné {item}", "successSavingChanges": "Modifications enregistrées avec succès !", "codeCopiedToClipboard": "Le code a été copié dans le presse-papiers !", "contentCopied": "Contenu copié !", "emptyCommentPrompt": "Le commentaire est vide. Souhaitez-vous définir le commentaire comme \\"vide\\" ?", "confirmDeleteLinkData": "Le lien {item} a été détecté ! Êtes-vous sûr de vouloir supprimer les données de ce lien ? Toutes les conversations et les commentaires seront supprimés !", "confirmCurrentURL": "Le lien {item} a été détecté ! Êtes-vous sûr ?", "commentSetToEmpty": "Commentaire défini comme \\"vide\\"", "enterCommentFirst": "Veuillez d'abord entrer un commentaire", "commentSaved": "Commentaire sauvegardé", "conversationDataDeleted": "Données de conversation supprimées avec succès", "executeSearchWithQuery": "Exécuter la recherche : {item}", "searchPlaceholder": "Entrez des mots-clés pour chercher...", "searchButton": "Rechercher", "searchInContent": "Contenu de la conversation", "searchInComments": "Commentaires des utilisateurs", "searchInBoth": "Contenu et commentaires", "goToPrevious": "Aller au précédent", "goToNext": "Aller au suivant", "numberOfMatches": "{matches} correspondances trouvées", "nodeDetails": "Détails", "enterComment": "Veuillez Entrer un Commentaire", "userCommentSave": "Enregistrer", "userCommentCancel": "Annuler", "userCommentClear": "Effacer", "openAdminPanel": "Ouvrir le Panneau Administratif", "allCategoriesFilter": "Toutes les Catégories", "conversationTitle": "Titre", "actionOptions": "Options", "conversationCategory": "Catégorie", "conversationTags": "Balises" }, "he": { "chatTreeRunning": "🥳🌳ChatTree🌳 פועל!🎉", "updateCurrentConversationTree": "🌈 עדכן את עץ השיחה הנוכחי 🌈", "adjustBackgroundColorAndOpacity": "🎨 כוונן את צבע הרקע והשקיפות 🎨", "toggleConversationTree": "🌳 הצג/הסתר עץ שיחה 🌳", "noDatabaseAndCreationFailed": "אין מסד נתונים ויצירתו נכשלה! סקריפט יוצא!", "mismatchedLink": "הקישור אינו תואם, אנא רענן את הדף!", "startUpdatingConversationTree": "התחל לעדכן את עץ השיחה", "selectedItem": "בחרת {item}", "successSavingChanges": "השינויים נשמרו בהצלחה!", "codeCopiedToClipboard": "הקוד הועתק ללוח!", "contentCopied": "התוכן הועתק!", "emptyCommentPrompt": "ההערה ריקה. האם תרצה להגדיר את ההערה כ\\"ריקה\\"?", "confirmDeleteLinkData": "הקישור {item} זוהה! האם אתה בטוח שאתה רוצה למחוק את נתוני הקישור הזה? כל השיחות וההערות יימחקו!", "confirmCurrentURL": "הקישור {item} זוהה! האם אתה בטוח?", "commentSetToEmpty": "ההערה הוגדרה כ\\"ריקה\\"", "enterCommentFirst": "אנא הזן הערה קודם", "commentSaved": "ההערה נשמרה", "conversationDataDeleted": "נתוני השיחה נמחקו בהצלחה", "executeSearchWithQuery": "בצע חיפוש: {item}", "searchPlaceholder": "הזן מילות חיפוש...", "searchButton": "חפש", "searchInContent": "תוכן השיחה", "searchInComments": "הערות משתמש", "searchInBoth": "תוכן והערות", "goToPrevious": "עבור לקודם", "goToNext": "עבור להבא", "numberOfMatches": "{matches} תוצאות", "nodeDetails": "פרטים", "enterComment": "אנא הקלד תגובה", "userCommentSave": "שמור", "userCommentCancel": "בטל", "userCommentClear": "נקה", "openAdminPanel": "פתח לוח בקרה", "allCategoriesFilter": "כל הקטגוריות", "conversationTitle": "כותרת", "actionOptions": "אפשרויות", "conversationCategory": "קטגוריה", "conversationTags": "תגיות" }, "hu": { "chatTreeRunning": "🥳🌳ChatTree🌳 fut!🎉", "updateCurrentConversationTree": "🌈 Frissítsd az aktuális beszélgetésfát 🌈", "adjustBackgroundColorAndOpacity": "🎨 Állítsd be a háttérszín és az átlátszóság értékeit 🎨", "toggleConversationTree": "🌳 Beszélgetésfa megjelenítése/elrejtése 🌳", "noDatabaseAndCreationFailed": "Nincs adatbázis és a létrehozás sikertelen! A szkript kilép!", "mismatchedLink": "A link nem egyezik, kérjük, frissítse az oldalt!", "startUpdatingConversationTree": "Beszélgetésfa frissítésének kezdése", "selectedItem": "Kiválasztott elem: {item}", "successSavingChanges": "A változások mentése sikerült!", "codeCopiedToClipboard": "A kód vágólapra másolva!", "contentCopied": "A tartalom másolva!", "emptyCommentPrompt": "A megjegyzés üres. Beállítja \\"üres\\"-ként?", "confirmDeleteLinkData": "A {item} link észlelve! Biztosan törli e link adatát? Minden beszélgetés és megjegyzés törlődik!", "confirmCurrentURL": "A {item} link észlelve! Biztos benne?", "commentSetToEmpty": "A megjegyzés \\"üres\\"-re van állítva", "enterCommentFirst": "Kérjük, először írja be a megjegyzést", "commentSaved": "Megjegyzés mentve", "conversationDataDeleted": "A beszélgetés adatai sikeresen törölve", "executeSearchWithQuery": "Keresés indítása: {item}", "searchPlaceholder": "Írja be a keresendő kifejezéseket...", "searchButton": "Keresés", "searchInContent": "Beszélgetés tartalma", "searchInComments": "Felhasználói megjegyzések", "searchInBoth": "Tartalom és megjegyzések", "goToPrevious": "Ugrás az előzőre", "goToNext": "Ugrás a következőre", "numberOfMatches": "{matches} találat", "nodeDetails": "Részletek", "enterComment": "Kérjük, írja be a megjegyzést", "userCommentSave": "Mentés", "userCommentCancel": "Mégse", "userCommentClear": "Törlés", "openAdminPanel": "Admin Panel Megnyitása", "allCategoriesFilter": "Minden Kategória", "conversationTitle": "Cím", "actionOptions": "Műveletek", "conversationCategory": "Kategória", "conversationTags": "Címkék" }, "id": { "chatTreeRunning": "🥳🌳ChatTree🌳 Sedang Berjalan!🎉", "updateCurrentConversationTree": "🌈 Perbarui Pohon Percakapan Saat Ini 🌈", "adjustBackgroundColorAndOpacity": "🎨 Sesuaikan Warna Latar Belakang dan Kecerahan 🎨", "toggleConversationTree": "🌳 Tampilkan/Sembunyikan Pohon Percakapan 🌳", "noDatabaseAndCreationFailed": "Tidak ada database, dan pembuatan gagal! Script keluar!", "mismatchedLink": "Tautan tidak cocok, silakan segarkan halaman!", "startUpdatingConversationTree": "Mulai memperbarui Pohon Percakapan", "selectedItem": "Anda memilih {item}", "successSavingChanges": "Berhasil Menyimpan Perubahan!", "codeCopiedToClipboard": "Kode disalin ke papan klip!", "contentCopied": "Konten disalin!", "emptyCommentPrompt": "Komentar kosong. Apakah Anda ingin mengatur komentar ke \\"kosong\\"?", "confirmDeleteLinkData": "Tautan {item} terdeteksi! Apakah Anda yakin ingin menghapus data tautan ini? Semua konten percakapan dan komentar akan dihapus!", "confirmCurrentURL": "Tautan {item} terdeteksi! Apakah Anda yakin?", "commentSetToEmpty": "Komentar diatur ke \\"kosong\\"", "enterCommentFirst": "Silakan masukkan komentar terlebih dahulu", "commentSaved": "Komentar disimpan", "conversationDataDeleted": "Data Percakapan Berhasil Dihapus", "executeSearchWithQuery": "Melakukan pencarian: {item}", "searchPlaceholder": "Masukkan kata kunci pencarian...", "searchButton": "Cari", "searchInContent": "Konten Percakapan", "searchInComments": "Komentar Pengguna", "searchInBoth": "Konten dan Komentar", "goToPrevious": "Pergi ke Sebelumnya", "goToNext": "Pergi ke Berikutnya", "numberOfMatches": "{matches} hasil pencarian", "nodeDetails": "Detail", "enterComment": "Silakan masukkan komentar", "userCommentSave": "Simpan", "userCommentCancel": "Batal", "userCommentClear": "Hapus", "openAdminPanel": "Buka Panel Admin", "allCategoriesFilter": "Semua Kategori", "conversationTitle": "Judul", "actionOptions": "Opsi", "conversationCategory": "Kategori", "conversationTags": "Tag" }, "it": { "chatTreeRunning": "🥳🌳ChatTree🌳 in esecuzione!🎉", "updateCurrentConversationTree": "🌈 Aggiorna l'albero di conversazione corrente 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajusta il colore di sfondo e l'opacità 🎨", "toggleConversationTree": "🌳 Mostra/Nascondi l'albero di conversazione 🌳", "noDatabaseAndCreationFailed": "Nessun database e la creazione non è riuscita! Script interrotto!", "mismatchedLink": "Il link non corrisponde, si prega di aggiornare la pagina!", "startUpdatingConversationTree": "Inizia l'aggiornamento dell'albero di conversazione", "selectedItem": "Hai selezionato {item}", "successSavingChanges": "Modifiche salvate con successo!", "codeCopiedToClipboard": "Codice copiato negli appunti!", "contentCopied": "Contenuto copiato!", "emptyCommentPrompt": "Il commento è vuoto. Vuoi impostare il commento su 'vuoto'?", "confirmDeleteLinkData": "Link rilevato {item}! Sei sicuro di voler eliminare le informazioni di questo link? Tutti i contenuti della conversazione e i commenti verranno eliminati!", "confirmCurrentURL": "Link rilevato {item}! Confermi?", "commentSetToEmpty": "Il commento è stato impostato su 'vuoto'", "enterCommentFirst": "Per favore, inserisci prima un commento", "commentSaved": "Commento salvato", "conversationDataDeleted": "Dati della conversazione eliminati con successo", "executeSearchWithQuery": "Esegui ricerca: {item}", "searchPlaceholder": "Inserisci parole chiave per la ricerca...", "searchButton": "Cerca", "searchInContent": "Contenuto della conversazione", "searchInComments": "Commenti degli utenti", "searchInBoth": "Contenuti e commenti", "goToPrevious": "Vai al precedente", "goToNext": "Vai al successivo", "numberOfMatches": "{matches} corrispondenze trovate", "nodeDetails": "Dettagli", "enterComment": "Per favore, inserisci un commento", "userCommentSave": "Salva", "userCommentCancel": "Annulla", "userCommentClear": "Cancella", "openAdminPanel": "Apri Pannello di Amministrazione", "allCategoriesFilter": "Tutte le Categorie", "conversationTitle": "Titolo", "actionOptions": "Opzioni", "conversationCategory": "Categoria", "conversationTags": "Tag" }, "ja": { "chatTreeRunning": "🥳🌳ChatTree🌳実行中!🎉", "updateCurrentConversationTree": "🌈 現在の会話ツリーを更新 🌈", "adjustBackgroundColorAndOpacity": "🎨 背景色と透明度を調整 🎨", "toggleConversationTree": "🌳 会話ツリーを表示/非表示 🌳", "noDatabaseAndCreationFailed": "データベースがなく、作成に失敗しました!スクリプトを終了します!", "mismatchedLink": "リンクが一致しません、ページを更新してください!", "startUpdatingConversationTree": "会話ツリーの更新を開始", "selectedItem": "{item}を選択しました", "successSavingChanges": "変更の保存に成功しました!", "codeCopiedToClipboard": "コードがクリップボードにコピーされました!", "contentCopied": "内容がコピーされました!", "emptyCommentPrompt": "コメントが空です。コメントを'空'に設定しますか?", "confirmDeleteLinkData": "{item}のリンクが検出されました! このリンクの情報を削除してもよろしいですか? すべての会話内容とコメントが削除されます!", "confirmCurrentURL": "{item}のリンクが検出されました!よろしいですか?", "commentSetToEmpty": "コメントが'空'に設定されました", "enterCommentFirst": "先にコメントを入力してください", "commentSaved": "コメントが保存されました", "conversationDataDeleted": "会話データが正常に削除されました", "executeSearchWithQuery": "検索を実行:{item}", "searchPlaceholder": "検索キーワードを入力...", "searchButton": "検索", "searchInContent": "会話の内容", "searchInComments": "ユーザーコメント", "searchInBoth": "内容とコメント", "goToPrevious": "前へ", "goToNext": "次へ", "numberOfMatches": "一致する項目{matches}件", "nodeDetails": "詳細", "enterComment": "コメントを入力してください", "userCommentSave": "保存", "userCommentCancel": "キャンセル", "userCommentClear": "クリア", "openAdminPanel": "管理パネルを開く", "allCategoriesFilter": "すべてのカテゴリ", "conversationTitle": "タイトル", "actionOptions": "オプション", "conversationCategory": "カテゴリ", "conversationTags": "タグ" }, "ka": { "chatTreeRunning": "🥳🌳ChatTree🌳 გაშვებულია!🎉", "updateCurrentConversationTree": "🌈 განაახლეთ ამჟამინდელი სასაუბრო ხე 🌈", "adjustBackgroundColorAndOpacity": "🎨 გაასწორეთ ფონის ფერი და გამჭვირვალობა 🎨", "toggleConversationTree": "🌳 ჩვენება/დამალვა სასაუბრო ხე 🌳", "noDatabaseAndCreationFailed": "არ არის მონაცემთა ბაზა და შექმნა ვერ ხერხდება! სკრიპტი გამოირიყება!", "mismatchedLink": "ბმული არ ემთხვევა, გთხოვთ, განაახლოთ გვერდი!", "startUpdatingConversationTree": "სასაუბრო ხის განახლების დაწყება", "selectedItem": "თქვენ აირჩიეთ {item}", "successSavingChanges": "ცვლილებები წარმატებით შენახულია!", "codeCopiedToClipboard": "კოდი კოპირებულია ბუფერში!", "contentCopied": "კონტენტი კოპირებულია!", "emptyCommentPrompt": "კომენტარი ცარიელია. გსურთ კომენტარი 'ცარიელი' განსაზღვროთ?", "confirmDeleteLinkData": "{item} ბმული აღმოჩენილია! დარწმუნებული ხართ, რომ გსურთ ამ ბმულის ინფორმაციის წაშლა? ყველა სასაუბრო კონტენტი და კომენტარი წაიშლება!", "confirmCurrentURL": "{item} ბმული აღმოჩენილია! დარწმუნებული ხართ?", "commentSetToEmpty": "კომენტარი 'ცარიელია' განსაზღვრულია", "enterCommentFirst": "გთხოვთ, ჯერ შეიყვანეთ კომენტარი", "commentSaved": "კომენტარი შენახულია", "conversationDataDeleted": "სასაუბრო მონაცემები წარმატებით წაიშალა", "executeSearchWithQuery": "ძიება შესრულება: {item}", "searchPlaceholder": "შეიყვანეთ ძიების სიტყვები...", "searchButton": "ძებნა", "searchInContent": "კონვერსიის კონტენტი", "searchInComments": "მომხმარებლის კომენტარები", "searchInBoth": "კონტენტი და კომენტარები", "goToPrevious": "წინაში გადასვლა", "goToNext": "შემდეგში გადასვლა", "numberOfMatches": "{matches} შესატყვისი", "nodeDetails": "დეტალები", "enterComment": "გთხოვთ შეიყვანოთ კომენტარი", "userCommentSave": "შენახვა", "userCommentCancel": "გაუქმება", "userCommentClear": "გასუფთავება", "openAdminPanel": "ადმინის პანელის გახსნა", "allCategoriesFilter": "ყველა კატეგორია", "conversationTitle": "სათაური", "actionOptions": "ოფციები", "conversationCategory": "კატეგორია", "conversationTags": "თეგები" }, "ko": { "chatTreeRunning": "🥳🌳ChatTree🌳 실행 중!🎉", "updateCurrentConversationTree": "🌈 현재 대화 트리 업데이트 🌈", "adjustBackgroundColorAndOpacity": "🎨 배경색과 투명도 조정 🎨", "toggleConversationTree": "🌳 대화 트리 표시/숨기기 🌳", "noDatabaseAndCreationFailed": "데이터베이스가 없으며 생성에 실패했습니다! 스크립트 종료!", "mismatchedLink": "링크가 일치하지 않습니다. 페이지를 새로고침하세요!", "startUpdatingConversationTree": "대화 트리 업데이트 시작", "selectedItem": "{item}를 선택했습니다", "successSavingChanges": "변경 사항이 성공적으로 저장되었습니다!", "codeCopiedToClipboard": "코드가 클립보드에 복사되었습니다!", "contentCopied": "내용이 복사되었습니다!", "emptyCommentPrompt": "댓글이 비어 있습니다. 댓글을 '비움'으로 설정하시겠습니까?", "confirmDeleteLinkData": "{item} 링크를 감지했습니다! 이 링크의 정보를 삭제하시겠습니까? 모든 대화 내용과 댓글이 삭제됩니다!", "confirmCurrentURL": "{item} 링크를 감지했습니다! 확인하시겠습니까?", "commentSetToEmpty": "댓글이 '비움'으로 설정되었습니다", "enterCommentFirst": "먼저 댓글을 입력해주세요", "commentSaved": "댓글이 저장되었습니다", "conversationDataDeleted": "대화 데이터가 성공적으로 삭제되었습니다", "executeSearchWithQuery": "검색 실행: {item}", "searchPlaceholder": "검색어를 입력하세요...", "searchButton": "검색", "searchInContent": "대화 내용", "searchInComments": "사용자 코멘트", "searchInBoth": "내용과 코멘트", "goToPrevious": "이전으로", "goToNext": "다음으로", "numberOfMatches": "{matches}개의 일치 항목", "nodeDetails": "세부 정보", "enterComment": "댓글을 입력하세요", "userCommentSave": "저장", "userCommentCancel": "취소", "userCommentClear": "지우기", "openAdminPanel": "관리 패널", "allCategoriesFilter": "모든 카테고리", "conversationTitle": "제목", "actionOptions": "작업", "conversationCategory": "카테고리", "conversationTags": "태그" }, "nb": { "chatTreeRunning": "🥳🌳ChatTree🌳 er i gang! 🎉", "updateCurrentConversationTree": "🌈 Oppdater gjeldende samtale-tre 🌈", "adjustBackgroundColorAndOpacity": "🎨 Juster bakgrunnsfarge og opasitet 🎨", "toggleConversationTree": "🌳 Vis/Skjul samtale-tre 🌳", "noDatabaseAndCreationFailed": "Ingen database, og opprettelsen mislyktes! Skriptet avslutter!", "mismatchedLink": "Lenken stemmer ikke, vennligst oppfrisk siden!", "startUpdatingConversationTree": "Begynner å oppdatere samtale-treet", "selectedItem": "Du valgte {item}", "successSavingChanges": "Endringene ble lagret!", "codeCopiedToClipboard": "Koden er kopiert til utklippstavlen!", "contentCopied": "Innholdet er kopiert!", "emptyCommentPrompt": "Kommentaren er tom. Vil du sette kommentaren til \\"tom\\"?", "confirmDeleteLinkData": "Linken er {item}! Er du sikker på at du vil slette denne linkens informasjon? All samtale og kommentarinnhold vil bli slettet!", "confirmCurrentURL": "Linken er {item}! Er du sikker?", "commentSetToEmpty": "Kommentaren er satt til \\"tom\\"", "enterCommentFirst": "Vennligst skriv en kommentar først", "commentSaved": "Kommentaren er lagret", "conversationDataDeleted": "Samtaledata er slettet", "executeSearchWithQuery": "Utfør søk: {item}", "searchPlaceholder": "Skriv inn søkeord...", "searchButton": "Søk", "searchInContent": "Samtaleinnhold", "searchInComments": "Brukerkommentarer", "searchInBoth": "Innhold og kommentarer", "goToPrevious": "Gå til forrige", "goToNext": "Gå til neste", "numberOfMatches": "{matches} treff", "nodeDetails": "Detaljer", "enterComment": "Vennligst skriv en kommentar", "userCommentSave": "Lagre", "userCommentCancel": "Avbryt", "userCommentClear": "Tøm", "openAdminPanel": "Administrasjonspanel", "allCategoriesFilter": "Alle kategorier", "conversationTitle": "Tittel", "actionOptions": "Handlinger", "conversationCategory": "Kategori", "conversationTags": "Merkelapper" }, "nl": { "chatTreeRunning": "🥳🌳ChatTree🌳 is actief! 🎉", "updateCurrentConversationTree": "🌈 Huidige gespreksboom bijwerken 🌈", "adjustBackgroundColorAndOpacity": "🎨 Achtergrondkleur en opaciteit aanpassen 🎨", "toggleConversationTree": "🌳 Gespreksboom weergeven/verbergen 🌳", "noDatabaseAndCreationFailed": "Geen database en het aanmaken is mislukt! Script wordt afgesloten!", "mismatchedLink": "Link komt niet overeen, ververs de pagina!", "startUpdatingConversationTree": "Begint met het bijwerken van de gespreksboom", "selectedItem": "Je hebt {item} geselecteerd", "successSavingChanges": "Wijzigingen succesvol opgeslagen!", "codeCopiedToClipboard": "Code gekopieerd naar klembord!", "contentCopied": "Inhoud gekopieerd!", "emptyCommentPrompt": "De opmerking is leeg. Wil je de opmerking instellen op \\"leeg\\"?", "confirmDeleteLinkData": "Gedetecteerde link is {item}! Weet je zeker dat je de informatie van deze link wilt verwijderen? Alle gespreks- en opmerkingsinhoud zal worden verwijderd!", "confirmCurrentURL": "Gedetecteerde link is {item}! Zeker weten?", "commentSetToEmpty": "Opmerking is ingesteld op \\"leeg\\"", "enterCommentFirst": "Voer eerst een opmerking in", "commentSaved": "Opmerking opgeslagen", "conversationDataDeleted": "Gespreksgegevens succesvol verwijderd", "executeSearchWithQuery": "Zoekopdracht uitvoeren: {item}", "searchPlaceholder": "Voer zoekwoorden in...", "searchButton": "Zoeken", "searchInContent": "In gespreksinhoud", "searchInComments": "In gebruikersopmerkingen", "searchInBoth": "In inhoud en opmerkingen", "goToPrevious": "Ga naar vorige", "goToNext": "Ga naar volgende", "numberOfMatches": "{matches} overeenkomsten", "nodeDetails": "Details", "enterComment": "Voer alstublieft een opmerking in", "userCommentSave": "Opslaan", "userCommentCancel": "Annuleren", "userCommentClear": "Wissen", "openAdminPanel": "Beheerpaneel", "allCategoriesFilter": "Alle categorieën", "conversationTitle": "Titel", "actionOptions": "Acties", "conversationCategory": "Categorie", "conversationTags": "Labels" }, "pl": { "chatTreeRunning": "🥳🌳ChatTree🌳 jest uruchomiony! 🎉", "updateCurrentConversationTree": "🌈 Aktualizuj aktualne drzewo rozmów 🌈", "adjustBackgroundColorAndOpacity": "🎨 Dostosuj kolor tła i przezroczystość 🎨", "toggleConversationTree": "🌳 Pokaż/ukryj drzewo rozmów 🌳", "noDatabaseAndCreationFailed": "Brak bazy danych, a jej utworzenie nie powiodło się! Skrypt zakończy działanie!", "mismatchedLink": "Link nie pasuje, odśwież stronę!", "startUpdatingConversationTree": "Rozpoczyna aktualizację drzewa rozmów", "selectedItem": "Wybrałeś {item}", "successSavingChanges": "Zmiany zostały zapisane!", "codeCopiedToClipboard": "Kod skopiowany do schowka!", "contentCopied": "Treść skopiowana!", "emptyCommentPrompt": "Komentarz jest pusty. Czy chcesz ustawić komentarz na \\"pusty\\"?", "confirmDeleteLinkData": "Wykryto link {item}! Czy na pewno chcesz usunąć informacje z tego linku? Wszystkie treści rozmów i komentarze zostaną usunięte!", "confirmCurrentURL": "Wykryto link {item}! Czy jesteś pewien?", "commentSetToEmpty": "Komentarz został ustawiony na \\"pusty\\"", "enterCommentFirst": "Najpierw wprowadź komentarz", "commentSaved": "Komentarz został zapisany", "conversationDataDeleted": "Dane rozmowy zostały pomyślnie usunięte", "executeSearchWithQuery": "Wykonaj wyszukiwanie: {item}", "searchPlaceholder": "Wprowadź słowa kluczowe...", "searchButton": "Szukaj", "searchInContent": "W treści rozmowy", "searchInComments": "W komentarzach użytkowników", "searchInBoth": "W treści i komentarzach", "goToPrevious": "Przejdź do poprzedniego", "goToNext": "Przejdź do następnego", "numberOfMatches": "{matches} dopasowań", "nodeDetails": "Szczegóły", "enterComment": "Proszę wprowadzić komentarz", "userCommentSave": "Zapisz", "userCommentCancel": "Anuluj", "userCommentClear": "Wyczyść", "openAdminPanel": "Panel administracyjny", "allCategoriesFilter": "Wszystkie kategorie", "conversationTitle": "Tytuł", "actionOptions": "Operacje", "conversationCategory": "Kategoria", "conversationTags": "Tagi" }, "pt-PT": { "chatTreeRunning": "🥳🌳ChatTree🌳 em execução! 🎉", "updateCurrentConversationTree": "🌈 Atualizar a árvore de conversação atual 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajustar a cor de fundo e a opacidade 🎨", "toggleConversationTree": "🌳 Mostrar/ocultar árvore de conversação 🌳", "noDatabaseAndCreationFailed": "Sem base de dados e a criação falhou! Script a encerrar!", "mismatchedLink": "Link não corresponde, por favor atualize a página!", "startUpdatingConversationTree": "A iniciar a atualização da árvore de conversação", "selectedItem": "Selecionou {item}", "successSavingChanges": "Alterações guardadas com sucesso!", "codeCopiedToClipboard": "Código copiado para a área de transferência!", "contentCopied": "Conteúdo copiado!", "emptyCommentPrompt": "O comentário está vazio. Deseja definir o comentário como \\"vazio\\"?", "confirmDeleteLinkData": "O link detetado é {item}! Tem a certeza que quer eliminar as informações deste link? Todo o conteúdo da conversa e comentários serão eliminados!", "confirmCurrentURL": "O link detetado é {item}! Confirma?", "commentSetToEmpty": "O comentário foi definido como \\"vazio\\"", "enterCommentFirst": "Por favor, insira um comentário primeiro", "commentSaved": "Comentário guardado", "conversationDataDeleted": "Dados de conversação eliminados com sucesso", "executeSearchWithQuery": "Executar pesquisa: {item}", "searchPlaceholder": "Insira palavras-chave...", "searchButton": "Pesquisar", "searchInContent": "No conteúdo da conversa", "searchInComments": "Nos comentários do usuário", "searchInBoth": "Em conteúdo e comentários", "goToPrevious": "Ir para o anterior", "goToNext": "Ir para o próximo", "numberOfMatches": "{matches} correspondências", "nodeDetails": "Detalhes", "enterComment": "Insira um comentário", "userCommentSave": "Guardar", "userCommentCancel": "Cancelar", "userCommentClear": "Limpar", "openAdminPanel": "Painel de administração", "allCategoriesFilter": "Todas as categorias", "conversationTitle": "Título", "actionOptions": "Opções", "conversationCategory": "Categoria", "conversationTags": "Etiquetas" }, "pt-BR": { "chatTreeRunning": "🥳🌳ChatTree🌳 em execução! 🎉", "updateCurrentConversationTree": "🌈 Atualizar a árvore de conversa atual 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajustar a cor de fundo e a opacidade 🎨", "toggleConversationTree": "🌳 Mostrar/esconder árvore de conversa 🌳", "noDatabaseAndCreationFailed": "Sem banco de dados e a criação falhou! Script encerrando!", "mismatchedLink": "Link não corresponde, por favor, atualize a página!", "startUpdatingConversationTree": "Começando a atualizar a árvore de conversa", "selectedItem": "Você selecionou {item}", "successSavingChanges": "Alterações salvas com sucesso!", "codeCopiedToClipboard": "Código copiado para a área de transferência!", "contentCopied": "Conteúdo copiado!", "emptyCommentPrompt": "O comentário está vazio. Você quer definir o comentário como \\"vazio\\"?", "confirmDeleteLinkData": "O link detectado é {item}! Tem certeza de que deseja excluir as informações deste link? Todo o conteúdo da conversa e os comentários serão excluídos!", "confirmCurrentURL": "O link detectado é {item}! Confirma?", "commentSetToEmpty": "O comentário foi definido como \\"vazio\\"", "enterCommentFirst": "Por favor, insira um comentário primeiro", "commentSaved": "Comentário salvo", "conversationDataDeleted": "Dados da conversa excluídos com sucesso", "executeSearchWithQuery": "Executar pesquisa: {item}", "searchPlaceholder": "Digite palavras-chave...", "searchButton": "Pesquisar", "searchInContent": "No conteúdo da conversa", "searchInComments": "Nos comentários do usuário", "searchInBoth": "Em conteúdo e comentários", "goToPrevious": "Ir para o anterior", "goToNext": "Ir para o próximo", "numberOfMatches": "{matches} correspondências", "nodeDetails": "Detalhes", "enterComment": "Digite um comentário", "userCommentSave": "Salvar", "userCommentCancel": "Cancelar", "userCommentClear": "Limpar", "openAdminPanel": "Painel de administração", "allCategoriesFilter": "Todas as categorias", "conversationTitle": "Título", "actionOptions": "Opções", "conversationCategory": "Categoria", "conversationTags": "Tags" }, "ro": { "chatTreeRunning": "🥳🌳ChatTree🌳 este în funcțiune!🎉", "updateCurrentConversationTree": "🌈 Actualizați arborele conversației curente 🌈", "adjustBackgroundColorAndOpacity": "🎨 Ajustați culoarea de fundal și opacitatea 🎨", "toggleConversationTree": "🌳 Afișați/Ascundeți arborele conversației 🌳", "noDatabaseAndCreationFailed": "Nu există bază de date și crearea a eșuat! Scriptul se închide!", "mismatchedLink": "Link-ul nu se potrivește, vă rugăm să reîmprospătați pagina!", "startUpdatingConversationTree": "Începeți să actualizați arborele conversației", "selectedItem": "Ați selectat {item}", "successSavingChanges": "Modificările au fost salvate cu succes!", "codeCopiedToClipboard": "Codul a fost copiat în clipboard!", "contentCopied": "Conținutul a fost copiat!", "emptyCommentPrompt": "Comentariul este gol. Doriți să setați comentariul ca \\"gol\\"?", "confirmDeleteLinkData": "Link detectat ca fiind {item}! Confirmați ștergerea informațiilor acestui link? Tot conținutul conversației și comentariile vor fi șterse!", "confirmCurrentURL": "Link detectat ca fiind {item}! Confirmați?", "commentSetToEmpty": "Comentariu setat ca \\"gol\\"", "enterCommentFirst": "Vă rugăm să introduceți mai întâi un comentariu", "commentSaved": "Comentariu salvat", "conversationDataDeleted": "Datele conversației au fost șterse cu succes", "executeSearchWithQuery": "Executați căutarea: {item}", "searchPlaceholder": "Introduceți cuvinte cheie...", "searchButton": "Căutare", "searchInContent": "În conținutul conversației", "searchInComments": "În comentariile utilizatorului", "searchInBoth": "În conținut și comentarii", "goToPrevious": "Mergi la precedentul", "goToNext": "Mergi la următorul", "numberOfMatches": "{matches} potriviri", "nodeDetails": "Detalii", "enterComment": "Introduceți un comentariu", "userCommentSave": "Salvați", "userCommentCancel": "Anulați", "userCommentClear": "Șterge", "openAdminPanel": "Panou administrativ", "allCategoriesFilter": "Toate categoriile", "conversationTitle": "Titlu", "actionOptions": "Opțiuni", "conversationCategory": "Categorie", "conversationTags": "Etichete" }, "ru": { "chatTreeRunning": "🥳🌳ChatTree🌳 запущен!🎉", "updateCurrentConversationTree": "🌈 Обновить текущее дерево беседы 🌈", "adjustBackgroundColorAndOpacity": "🎨 Настроить цвет фона и прозрачность 🎨", "toggleConversationTree": "🌳 Показать/Скрыть дерево беседы 🌳", "noDatabaseAndCreationFailed": "База данных отсутствует, и создание не удалось! Скрипт выходит!", "mismatchedLink": "Ссылка не совпадает, пожалуйста, обновите страницу!", "startUpdatingConversationTree": "Начать обновление дерева беседы", "selectedItem": "Вы выбрали {item}", "successSavingChanges": "Изменения успешно сохранены!", "codeCopiedToClipboard": "Код скопирован в буфер обмена!", "contentCopied": "Содержимое скопировано!", "emptyCommentPrompt": "Комментарий пуст. Вы хотите установить комментарий как \\"пустой\\"?", "confirmDeleteLinkData": "Обнаружена ссылка {item}! Подтвердить удаление информации этой ссылки? Весь контент беседы и комментарии будут удалены!", "confirmCurrentURL": "Обнаружена ссылка {item}! Подтвердить?", "commentSetToEmpty": "Комментарий установлен как \\"пустой\\"", "enterCommentFirst": "Пожалуйста, сначала введите комментарий", "commentSaved": "Комментарий сохранен", "conversationDataDeleted": "Данные беседы успешно удалены", "executeSearchWithQuery": "Выполнить поиск: {item}", "searchPlaceholder": "Введите ключевые слова...", "searchButton": "Поиск", "searchInContent": "В контенте диалога", "searchInComments": "В комментариях пользователя", "searchInBoth": "В контенте и комментариях", "goToPrevious": "Перейти к предыдущему", "goToNext": "Перейти к следующему", "numberOfMatches": "{matches} совпадений", "nodeDetails": "Подробности", "enterComment": "Введите комментарий", "userCommentSave": "Сохранить", "userCommentCancel": "Отмена", "userCommentClear": "Очистить", "openAdminPanel": "Панель управления", "allCategoriesFilter": "Все категории", "conversationTitle": "Заголовок", "actionOptions": "Опции", "conversationCategory": "Категория", "conversationTags": "Теги" }, "sk": { "chatTreeRunning": "🥳🌳ChatTree🌳 beží!🎉", "updateCurrentConversationTree": "🌈 Aktualizovať aktuálny konverzačný strom 🌈", "adjustBackgroundColorAndOpacity": "🎨 Upraviť farbu pozadia a priehľadnosť 🎨", "toggleConversationTree": "🌳 Zobraziť/Skryť konverzačný strom 🌳", "noDatabaseAndCreationFailed": "Nie je databáza a vytvorenie zlyhalo! Skript končí!", "mismatchedLink": "Odkaz sa nezhoduje, prosím, obnovte stránku!", "startUpdatingConversationTree": "Začať aktualizovať konverzačný strom", "selectedItem": "Vybrali ste {item}", "successSavingChanges": "Zmeny boli úspešne uložené!", "codeCopiedToClipboard": "Kód bol skopírovaný do schránky!", "contentCopied": "Obsah bol skopírovaný!", "emptyCommentPrompt": "Komentár je prázdny. Chcete nastaviť komentár na \\"prázdny\\"?", "confirmDeleteLinkData": "Odkaz detekovaný ako {item}! Potvrdiť odstránenie informácií tohto odkazu? Všetok obsah konverzácie a komentáre budú odstránené!", "confirmCurrentURL": "Odkaz detekovaný ako {item}! Potvrdiť?", "commentSetToEmpty": "Komentár bol nastavený na \\"prázdny\\"", "enterCommentFirst": "Najprv prosím, zadajte komentár", "commentSaved": "Komentár bol uložený", "conversationDataDeleted": "Dáta konverzácie boli úspešne odstránené", "executeSearchWithQuery": "Vykonať vyhľadávanie: {item}", "searchPlaceholder": "Zadajte kľúčové slová pre vyhľadávanie...", "searchButton": "Hľadať", "searchInContent": "V obsahu konverzácie", "searchInComments": "V komentároch", "searchInBoth": "V obsahu a komentároch", "goToPrevious": "Prejsť na predchádzajúci", "goToNext": "Prejsť na ďalší", "numberOfMatches": "{matches} zhôd", "nodeDetails": "Detaily", "enterComment": "Zadajte komentár", "userCommentSave": "Uložiť", "userCommentCancel": "Zrušiť", "userCommentClear": "Vymazať", "openAdminPanel": "Administračný panel", "allCategoriesFilter": "Všetky kategórie", "conversationTitle": "Názov", "actionOptions": "Možnosti", "conversationCategory": "Kategória", "conversationTags": "Značky" }, "sr": { "chatTreeRunning": "🥳🌳ChatTree🌳 je pokrenut!🎉", "updateCurrentConversationTree": "🌈 Ažurirajte trenutno stablo razgovora 🌈", "adjustBackgroundColorAndOpacity": "🎨 Podesite boju pozadine i providnost 🎨", "toggleConversationTree": "🌳 Prikaži/Sakrij stablo razgovora 🌳", "noDatabaseAndCreationFailed": "Nema baze podataka, a kreiranje nije uspelo! Skript izlazi!", "mismatchedLink": "Link se ne podudara, molimo osvežite stranicu!", "startUpdatingConversationTree": "Počnite ažurirati stablo razgovora", "selectedItem": "Izabrali ste {item}", "successSavingChanges": "Promene su uspešno sačuvane!", "codeCopiedToClipboard": "Kod je kopiran u klipbord!", "contentCopied": "Sadržaj je kopiran!", "emptyCommentPrompt": "Komentar je prazan. Da li želite postaviti komentar kao \\"prazan\\"?", "confirmDeleteLinkData": "Link je detektovan kao {item}! Potvrditi brisanje informacija o ovom linku? Svi podaci razgovora i komentari će biti obrisani!", "confirmCurrentURL": "Link je detektovan kao {item}! Potvrditi?", "commentSetToEmpty": "Komentar je postavljen kao \\"prazan\\"", "enterCommentFirst": "Molimo prvo unesite komentar", "commentSaved": "Komentar je sačuvan", "conversationDataDeleted": "Podaci razgovora su uspešno obrisani", "executeSearchWithQuery": "Izvršiti pretragu: {item}", "searchPlaceholder": "Unesite ključne reči za pretragu...", "searchButton": "Pretraga", "searchInContent": "U sadržaju razgovora", "searchInComments": "U komentarima", "searchInBoth": "U sadržaju i komentarima", "goToPrevious": "Idi na prethodni", "goToNext": "Idi na sledeći", "numberOfMatches": "{matches} poklapanja", "nodeDetails": "Detalji", "enterComment": "Unesite komentar", "userCommentSave": "Sačuvaj", "userCommentCancel": "Otkaži", "userCommentClear": "Obriši", "openAdminPanel": "Administrativni panel", "allCategoriesFilter": "Sve kategorije", "conversationTitle": "Naslov", "actionOptions": "Opcije", "conversationCategory": "Kategorija", "conversationTags": "Tagovi" }, "sv": { "chatTreeRunning": "🥳🌳ChatTree🌳 är igång!🎉", "updateCurrentConversationTree": "🌈 Uppdatera aktuellt konversations träd 🌈", "adjustBackgroundColorAndOpacity": "🎨 Justera bakgrundsfärg och opacitet 🎨", "toggleConversationTree": "🌳 Visa/Dölj konversationsträd 🌳", "noDatabaseAndCreationFailed": "Ingen databas, och skapandet misslyckades! Skriptet avslutas!", "mismatchedLink": "Länken matchar inte, vänligen uppdatera sidan!", "startUpdatingConversationTree": "Börja uppdatera konversationsträdet", "selectedItem": "Du valde {item}", "successSavingChanges": "Ändringar sparade framgångsrikt!", "codeCopiedToClipboard": "Koden har kopierats till urklipp!", "contentCopied": "Innehållet har kopierats!", "emptyCommentPrompt": "Kommentaren är tom. Vill du ställa in kommentaren som \\"tom\\"?", "confirmDeleteLinkData": "Länk identifierad som {item}! Bekräfta radering av denna länks information? All konversationsinnehåll och kommentarer kommer att raderas!", "confirmCurrentURL": "Länk identifierad som {item}! Bekräfta?", "commentSetToEmpty": "Kommentaren är inställd på \\"tom\\"", "enterCommentFirst": "Vänligen skriv en kommentar först", "commentSaved": "Kommentar sparad", "conversationDataDeleted": "Konversationsdata har raderats framgångsrikt", "executeSearchWithQuery": "Utför sökning: {item}", "searchPlaceholder": "Ange sökord...", "searchButton": "Sök", "searchInContent": "I konversationsinnehållet", "searchInComments": "I kommentarer", "searchInBoth": "I både innehåll och kommentarer", "goToPrevious": "Gå till föregående", "goToNext": "Gå till nästa", "numberOfMatches": "{matches} träffar", "nodeDetails": "Detaljer", "enterComment": "Ange en kommentar", "userCommentSave": "Spara", "userCommentCancel": "Avbryt", "userCommentClear": "Rensa", "openAdminPanel": "Administrationspanel", "allCategoriesFilter": "Alla kategorier", "conversationTitle": "Titel", "actionOptions": "Åtgärder", "conversationCategory": "Kategori", "conversationTags": "Taggar" }, "th": { "chatTreeRunning": "🥳🌳ChatTree🌳กำลังทำงาน!🎉", "updateCurrentConversationTree": "🌈 อัปเดตต้นไม้การสนทนาปัจจุบัน 🌈", "adjustBackgroundColorAndOpacity": "🎨 ปรับสีพื้นหลังและความโปร่งใส 🎨", "toggleConversationTree": "🌳 แสดง/ซ่อนต้นไม้การสนทนา 🌳", "noDatabaseAndCreationFailed": "ไม่มีฐานข้อมูล และไม่สามารถสร้างสำเร็จ! สคริปต์ออก!", "mismatchedLink": "ลิงก์ไม่ตรงกัน โปรดรีเฟรชหน้า!", "startUpdatingConversationTree": "เริ่มต้นอัปเดตต้นไม้การสนทนา", "selectedItem": "คุณเลือก {item}", "successSavingChanges": "บันทึกการเปลี่ยนแปลงสำเร็จ!", "codeCopiedToClipboard": "คัดลอกโค้ดไปยังคลิปบอร์ดแล้ว!", "contentCopied": "คัดลอกเนื้อหาแล้ว!", "emptyCommentPrompt": "คอมเมนต์ว่างเปล่า คุณต้องการตั้งค่าคอมเมนต์เป็น \\"ว่างเปล่า\\" หรือไม่?", "confirmDeleteLinkData": "ตรวจพบลิงก์เป็น {item}! ยืนยันที่จะลบข้อมูลลิงก์นี้หรือไม่? ข้อมูลการสนทนาและคอมเมนต์ทั้งหมดจะถูกลบ!", "confirmCurrentURL": "ตรวจพบลิงก์เป็น {item}! ยืนยันหรือไม่?", "commentSetToEmpty": "ตั้งค่าคอมเมนต์เป็น \\"ว่างเปล่า\\"", "enterCommentFirst": "โปรดใส่คอมเมนต์ก่อน", "commentSaved": "บันทึกคอมเมนต์แล้ว", "conversationDataDeleted": "ลบข้อมูลการสนทนาสำเร็จ", "executeSearchWithQuery": "ดำเนินการค้นหา: {item}", "searchPlaceholder": "ป้อนคำค้นหา...", "searchButton": "ค้นหา", "searchInContent": "ในเนื้อหาการสนทนา", "searchInComments": "ในความคิดเห็น", "searchInBoth": "ทั้งเนื้อหาและความคิดเห็น", "goToPrevious": "ไปที่ก่อนหน้า", "goToNext": "ไปที่ถัดไป", "numberOfMatches": "{matches} รายการที่ตรงกัน", "nodeDetails": "รายละเอียด", "enterComment": "กรุณาใส่ความคิดเห็น", "userCommentSave": "บันทึก", "userCommentCancel": "ยกเลิก", "userCommentClear": "ล้าง", "openAdminPanel": "แผงการจัดการ", "allCategoriesFilter": "ทุกหมวดหมู่", "conversationTitle": "ชื่อเรื่อง", "actionOptions": "การดำเนินการ", "conversationCategory": "ประเภท", "conversationTags": "แท็ก" }, "tr": { "chatTreeRunning": "🥳🌳ChatTree🌳Çalışıyor!🎉", "updateCurrentConversationTree": "🌈 Güncel Konuşma Ağacını Güncelle 🌈", "adjustBackgroundColorAndOpacity": "🎨 Arka Plan Rengini ve Şeffaflığı Ayarla 🎨", "toggleConversationTree": "🌳 Konuşma Ağacını Göster/Gizle 🌳", "noDatabaseAndCreationFailed": "Veritabanı yok ve oluşturulamadı! Betik çıkıyor!", "mismatchedLink": "Bağlantı eşleşmiyor, lütfen sayfayı yenileyin!", "startUpdatingConversationTree": "Konuşma Ağacını Güncellemeye Başla", "selectedItem": "{item} seçildi", "successSavingChanges": "Değişiklikler Başarıyla Kaydedildi!", "codeCopiedToClipboard": "Kod panoya kopyalandı!", "contentCopied": "İçerik kopyalandı!", "emptyCommentPrompt": "Yorum boş. Yorumu \\"boş\\" olarak ayarlamak istiyor musunuz?", "confirmDeleteLinkData": "{item} bağlantısı tespit edildi! Bu bağlantının bilgilerini silmek istediğinizi onaylıyor musunuz? Tüm konuşma ve yorum içerikleri silinecek!", "confirmCurrentURL": "{item} bağlantısı tespit edildi! Onaylıyor musunuz?", "commentSetToEmpty": "Yorum \\"boş\\" olarak ayarlandı", "enterCommentFirst": "Lütfen önce bir yorum girin", "commentSaved": "Yorum kaydedildi", "conversationDataDeleted": "Konuşma Verileri Başarıyla Silindi", "executeSearchWithQuery": "Arama Yapılıyor: {item}", "searchPlaceholder": "Arama anahtar kelimelerini girin...", "searchButton": "Ara", "searchInContent": "Konuşma İçeriğinde", "searchInComments": "Yorumlarda", "searchInBoth": "İçerik ve Yorumlarda", "goToPrevious": "Öncekine Git", "goToNext": "Sonrakine Git", "numberOfMatches": "{matches} eşleşme", "nodeDetails": "Detaylar", "enterComment": "Bir yorum girin", "userCommentSave": "Kaydet", "userCommentCancel": "İptal", "userCommentClear": "Temizle", "openAdminPanel": "Yönetim Paneli", "allCategoriesFilter": "Tüm Kategoriler", "conversationTitle": "Başlık", "actionOptions": "İşlemler", "conversationCategory": "Kategori", "conversationTags": "Etiketler" }, "uk": { "chatTreeRunning": "🥳🌳ChatTree🌳в роботі!🎉", "updateCurrentConversationTree": "🌈 Оновити поточне дерево розмов 🌈", "adjustBackgroundColorAndOpacity": "🎨 Налаштувати колір фону та прозорість 🎨", "toggleConversationTree": "🌳 Показати/сховати дерево розмов 🌳", "noDatabaseAndCreationFailed": "Немає бази даних, і не вдалося створити! Скрипт виходить!", "mismatchedLink": "Посилання не збігається, будь ласка, оновіть сторінку!", "startUpdatingConversationTree": "Почати оновлення дерева розмов", "selectedItem": "Ви обрали {item}", "successSavingChanges": "Зміни успішно збережено!", "codeCopiedToClipboard": "Код скопійовано в буфер обміну!", "contentCopied": "Зміст скопійовано!", "emptyCommentPrompt": "Коментар порожній. Ви хочете встановити коментар як \\"порожній\\"?", "confirmDeleteLinkData": "Посилання виявлене як {item}! Підтверджуєте видалення цього посилання? Всі дані розмов і коментарі будуть видалені!", "confirmCurrentURL": "Посилання виявлене як {item}! Підтвердити?", "commentSetToEmpty": "Коментар встановлено як \\"порожній\\"", "enterCommentFirst": "Будь ласка, спочатку введіть коментар", "commentSaved": "Коментар збережено", "conversationDataDeleted": "Дані розмови успішно видалено", "executeSearchWithQuery": "Виконати пошук: {item}", "searchPlaceholder": "Введіть ключові слова для пошуку...", "searchButton": "Пошук", "searchInContent": "У вмісті розмови", "searchInComments": "У коментарях", "searchInBoth": "У вмісті та коментарях", "goToPrevious": "Перейти до попереднього", "goToNext": "Перейти до наступного", "numberOfMatches": "{matches} збігів", "nodeDetails": "Деталі", "enterComment": "Введіть коментар", "userCommentSave": "Зберегти", "userCommentCancel": "Скасувати", "userCommentClear": "Очистити", "openAdminPanel": "Панель адміністрування", "allCategoriesFilter": "Всі категорії", "conversationTitle": "Заголовок", "actionOptions": "Дії", "conversationCategory": "Категорія", "conversationTags": "Теги" }, "ug": { "chatTreeRunning": "🥳🌳ChatTree🌳ئىشلىتىلىۋاتىدۇ!🎉", "updateCurrentConversationTree": "🌈 نۆۋەتتىكي سۆھبەت تەرەككىنى يېڭىلاڭ 🌈", "adjustBackgroundColorAndOpacity": "🎨 فون رەڭگىنى ۋە شېفافلىقىنى تەڭشەڭ 🎨", "toggleConversationTree": "🌳 سۆھبەت تەرەككىنى كۆرسىتىش/يوشۇرۇش 🌳", "noDatabaseAndCreationFailed": "ماڭلىق يوق، ھەمدە قۇرۇلمىدى! سكرىپت چىقىدۇ!", "mismatchedLink": "ئۇلىنىش ماس كەلمىدى، تور بەتنى يېڭىلاڭ!", "startUpdatingConversationTree": "سۆھبەت تەرەككىنى يېڭىلاشنى باشلاڭ", "selectedItem": "سىز تاللىغان {item}", "successSavingChanges": "ئۆزگەرتىشنى ساقلاش مۇۋەپپەقىيەتلىك بولدى!", "codeCopiedToClipboard": "كود تەسۋىرلىتىش تاختىسىغا كۆچۈرۈلدى!", "contentCopied": "مەزمۇن كۆچۈرۈلدى!", "emptyCommentPrompt": "ئىزاھات قۇرۇق. \\"قۇرۇق\\" دەپ بەلگىلەمسىز؟", "confirmDeleteLinkData": "{item} ئۇلىنىش تەپلىگەن! بۇ ئۇلىنىشنىڭ ئۇچۇرىنى ئۆچۈرۈشنى جەزملەسىزمۇ؟ بارلىق سۆھبەت ۋە ئىزاھات ئۆچۈرۈلىدۇ!", "confirmCurrentURL": "{item} ئۇلىنىش تەپلىگەن! جەزملەسىزمۇ؟", "commentSetToEmpty": "ئىزاھات \\"قۇرۇق\\" دەپ بەلگىلەندى", "enterCommentFirst": "ئاۋۋال ئىزاھات كىرگۈزۈڭ", "commentSaved": "ئىزاھات ساقلاندى", "conversationDataDeleted": "سۆھبەت ئۇچۇرى مۇۋەپپەقىيەتلىك ئۆچۈرۈلدى", "executeSearchWithQuery": "ئىزدەشنى ئىجرا قىلىدۇ: {item}", "searchPlaceholder": "ئىزدەش سۆزىنى كىرگۈزۈڭ...", "searchButton": "ئىزدەش", "searchInContent": "سۆھبەت مەزمۇنىدا", "searchInComments": "ئىزاھاتتا", "searchInBoth": "مەزمۇن ۋە ئىزاھاتتا", "goToPrevious": "ئالدىنقىغا بېرىش", "goToNext": "كېيىنكىگە بېرىش", "numberOfMatches": "{matches} دەسلەپتىكى ئۇنۇم", "nodeDetails": "نود تەپسىلاتى", "enterComment": "ئىزاھات كىرگۈزۈڭ", "userCommentSave": "ساقلا", "userCommentCancel": "بىكار قىل", "userCommentClear": "تازىلا", "openAdminPanel": "باشقۇرۇش تاختىسى", "allCategoriesFilter": "بارلىق تۈرلەر", "conversationTitle": "تېما", "actionOptions": "مەشغۇلاتلار", "conversationCategory": "تۈر", "conversationTags": "بەلگىلەر" }, "vi": { "chatTreeRunning": "🥳🌳ChatTree🌳đang chạy!🎉", "updateCurrentConversationTree": "🌈 Cập nhật cây trò chuyện hiện tại 🌈", "adjustBackgroundColorAndOpacity": "🎨 Điều chỉnh màu nền và độ mờ 🎨", "toggleConversationTree": "🌳 Hiển thị/Ẩn cây trò chuyện 🌳", "noDatabaseAndCreationFailed": "Không có cơ sở dữ liệu và không thể tạo được! Script thoát!", "mismatchedLink": "Liên kết không khớp, vui lòng làm mới trang!", "startUpdatingConversationTree": "Bắt đầu cập nhật cây trò chuyện", "selectedItem": "Bạn đã chọn {item}", "successSavingChanges": "Lưu thay đổi thành công!", "codeCopiedToClipboard": "Mã đã được sao chép vào bảng tạm!", "contentCopied": "Nội dung đã được sao chép!", "emptyCommentPrompt": "Bình luận trống. Bạn có muốn đặt bình luận thành \\"trống\\" không?", "confirmDeleteLinkData": "Đã phát hiện liên kết là {item}! Bạn có chắc chắn muốn xóa thông tin của liên kết này không? Tất cả nội dung trò chuyện và bình luận sẽ bị xóa!", "confirmCurrentURL": "Đã phát hiện liên kết là {item}! Bạn có xác nhận không?", "commentSetToEmpty": "Bình luận đã được đặt thành \\"trống\\"", "enterCommentFirst": "Vui lòng nhập bình luận trước", "commentSaved": "Bình luận đã được lưu", "conversationDataDeleted": "Dữ liệu trò chuyện đã được xóa thành công", "executeSearchWithQuery": "Thực hiện tìm kiếm: {item}", "searchPlaceholder": "Nhập từ khóa tìm kiếm...", "searchButton": "Tìm kiếm", "searchInContent": "Trong nội dung cuộc trò chuyện", "searchInComments": "Trong bình luận", "searchInBoth": "Cả trong nội dung và bình luận", "goToPrevious": "Đi đến trước đó", "goToNext": "Đi đến tiếp theo", "numberOfMatches": "{matches} kết quả phù hợp", "nodeDetails": "Chi tiết nút", "enterComment": "Nhập bình luận", "userCommentSave": "Lưu", "userCommentCancel": "Hủy", "userCommentClear": "Xóa sạch", "openAdminPanel": "Bảng Quản trị", "allCategoriesFilter": "Tất cả danh mục", "conversationTitle": "Tiêu đề", "actionOptions": "Tùy chọn", "conversationCategory": "Danh mục", "conversationTags": "Thẻ" }, "zh": { "chatTreeRunning": "🥳🌳ChatTree🌳运行中!🎉", "updateCurrentConversationTree": "🌈 更新当前对话树 🌈", "adjustBackgroundColorAndOpacity": "🎨 调整背景颜色和透明度 🎨", "toggleConversationTree": "🌳 显示/隐藏对话树 🌳", "noDatabaseAndCreationFailed": "没有数据库,并且未能成功创建!脚本退出!", "mismatchedLink": "链接不匹配,请刷新页面!", "startUpdatingConversationTree": "开始更新对话树", "selectedItem": "您选择了{item}", "successSavingChanges": "更改保存成功!", "codeCopiedToClipboard": "代码已复制到剪贴板!", "contentCopied": "内容已复制!", "emptyCommentPrompt": "注释为空。是否将注释设置为\\"空\\"?", "confirmDeleteLinkData": "监测到链接为 {item}!确认要删除这个链接的信息吗?所有的对话内容和注释都将被删除!", "confirmCurrentURL": "监测到链接为 {item}!确认吗?", "commentSetToEmpty": "注释已设置为\\"空\\"", "enterCommentFirst": "请先输入注释内容", "commentSaved": "注释已保存", "conversationDataDeleted": "对话数据成功删除", "executeSearchWithQuery": "执行搜索:{item}", "searchPlaceholder": "输入搜索关键词...", "searchButton": "搜索", "searchInContent": "对话内容", "searchInComments": "用户注释", "searchInBoth": "内容与注释", "goToPrevious": "转到上一个", "goToNext": "转到下一个", "numberOfMatches": "{matches} 个匹配项", "nodeDetails": "详情", "enterComment": "请输入注释", "userCommentSave": "保存", "userCommentCancel": "取消", "userCommentClear": "清空", "openAdminPanel": "管理面板", "allCategoriesFilter": "所有分类", "conversationTitle": "标题", "actionOptions": "操作", "conversationCategory": "分类", "conversationTags": "标签" }, "zh-CN": { "chatTreeRunning": "🥳🌳ChatTree🌳运行中!🎉", "updateCurrentConversationTree": "🌈 更新当前对话树 🌈", "adjustBackgroundColorAndOpacity": "🎨 调整背景颜色和透明度 🎨", "toggleConversationTree": "🌳 显示/隐藏对话树 🌳", "noDatabaseAndCreationFailed": "没有数据库,并且未能成功创建!脚本退出!", "mismatchedLink": "链接不匹配,请刷新页面!", "startUpdatingConversationTree": "开始更新对话树", "selectedItem": "您选择了{item}", "successSavingChanges": "更改保存成功!", "codeCopiedToClipboard": "代码已复制到剪贴板!", "contentCopied": "内容已复制!", "emptyCommentPrompt": "注释为空。是否将注释设置为\\"空\\"?", "confirmDeleteLinkData": "监测到链接为 {item}!确认要删除这个链接的信息吗?所有的对话内容和注释都将被删除!", "confirmCurrentURL": "监测到链接为 {item}!确认吗?", "commentSetToEmpty": "注释已设置为\\"空\\"", "enterCommentFirst": "请先输入注释内容", "commentSaved": "注释已保存", "conversationDataDeleted": "对话数据成功删除", "executeSearchWithQuery": "执行搜索:{item}", "searchPlaceholder": "输入搜索关键词...", "searchButton": "搜索", "searchInContent": "对话内容", "searchInComments": "用户注释", "searchInBoth": "内容与注释", "goToPrevious": "转到上一个", "goToNext": "转到下一个", "numberOfMatches": "{matches} 个匹配项", "nodeDetails": "详情", "enterComment": "请输入注释", "userCommentSave": "保存", "userCommentCancel": "取消", "userCommentClear": "清空", "openAdminPanel": "管理面板", "allCategoriesFilter": "所有分类", "conversationTitle": "标题", "actionOptions": "操作", "conversationCategory": "分类", "conversationTags": "标签" }, "zh-TW": { "chatTreeRunning": "🥳🌳ChatTree🌳運行中!🎉", "updateCurrentConversationTree": "🌈 更新當前對話樹 🌈", "adjustBackgroundColorAndOpacity": "🎨 調整背景顏色和透明度 🎨", "toggleConversationTree": "🌳 顯示/隱藏對話樹 🌳", "noDatabaseAndCreationFailed": "沒有資料庫,且未能成功創建!腳本退出!", "mismatchedLink": "鏈接不匹配,請刷新頁面!", "startUpdatingConversationTree": "開始更新對話樹", "selectedItem": "您選擇了{item}", "successSavingChanges": "更改保存成功!", "codeCopiedToClipboard": "代碼已複製到剪貼板!", "contentCopied": "內容已複製!", "emptyCommentPrompt": "註釋為空。是否將註釋設置為\\"空\\"?", "confirmDeleteLinkData": "監測到鏈接為 {item}!確認要刪除這個鏈接的信息嗎?所有的對話內容和註釋都將被刪除!", "confirmCurrentURL": "監測到鏈接為 {item}!確認嗎?", "commentSetToEmpty": "註釋已設置為\\"空\\"", "enterCommentFirst": "請先輸入註釋內容", "commentSaved": "註釋已保存", "conversationDataDeleted": "對話數據成功刪除", "executeSearchWithQuery": "執行搜索:{item}", "searchPlaceholder": "輸入搜索關鍵詞...", "searchButton": "搜索", "searchInContent": "對話內容", "searchInComments": "使用者註釋", "searchInBoth": "內容與註釋", "goToPrevious": "轉到上一個", "goToNext": "轉到下一個", "numberOfMatches": "{matches} 個匹配項", "nodeDetails": "詳情", "enterComment": "請輸入注釋", "userCommentSave": "保存", "userCommentCancel": "取消", "userCommentClear": "清空", "openAdminPanel": "管理面板", "allCategoriesFilter": "所有類別", "conversationTitle": "標題", "actionOptions": "操作選項", "conversationCategory": "類別", "conversationTags": "標籤" }, "zh-HK": { "chatTreeRunning": "🥳🌳ChatTree🌳運行中!🎉", "updateCurrentConversationTree": "🌈 更新當前對話樹 🌈", "adjustBackgroundColorAndOpacity": "🎨 調整背景顏色和透明度 🎨", "toggleConversationTree": "🌳 顯示/隱藏對話樹 🌳", "noDatabaseAndCreationFailed": "沒有資料庫,且未能成功創建!腳本退出!", "mismatchedLink": "鏈接不匹配,請刷新頁面!", "startUpdatingConversationTree": "開始更新對話樹", "selectedItem": "您選擇了{item}", "successSavingChanges": "更改保存成功!", "codeCopiedToClipboard": "代碼已複製到剪貼板!", "contentCopied": "內容已複製!", "emptyCommentPrompt": "註釋為空。是否將註釋設置為\\"空\\"?", "confirmDeleteLinkData": "監測到鏈接為 {item}!確認要刪除這個鏈接的信息嗎?所有的對話內容和註釋都將被刪除!", "confirmCurrentURL": "監測到鏈接為 {item}!確認嗎?", "commentSetToEmpty": "註釋已設置為\\"空\\"", "enterCommentFirst": "請先輸入註釋內容", "commentSaved": "註釋已保存", "conversationDataDeleted": "對話數據成功刪除", "executeSearchWithQuery": "執行搜索:{item}", "searchPlaceholder": "輸入搜尋關鍵詞...", "searchButton": "搜尋", "searchInContent": "對話內容", "searchInComments": "用戶註釋", "searchInBoth": "內容與註釋", "goToPrevious": "轉到上一個", "goToNext": "轉到下一個", "numberOfMatches": "{matches} 個匹配項", "nodeDetails": "詳情", "enterComment": "請輸入註釋", "userCommentSave": "儲存", "userCommentCancel": "取消", "userCommentClear": "清除", "openAdminPanel": "管理面板", "allCategoriesFilter": "所有類別", "conversationTitle": "標題", "actionOptions": "操作選項", "conversationCategory": "類別", "conversationTags": "標籤" }, "zh-SG": { "chatTreeRunning": "🥳🌳ChatTree🌳运行中!🎉", "updateCurrentConversationTree": "🌈 更新当前对话树 🌈", "adjustBackgroundColorAndOpacity": "🎨 调整背景颜色和透明度 🎨", "toggleConversationTree": "🌳 显示/隐藏对话树 🌳", "noDatabaseAndCreationFailed": "没有数据库,并且未能成功创建!脚本退出!", "mismatchedLink": "链接不匹配,请刷新页面!", "startUpdatingConversationTree": "开始更新对话树", "selectedItem": "您选择了{item}", "successSavingChanges": "更改保存成功!", "codeCopiedToClipboard": "代码已复制到剪贴板!", "contentCopied": "内容已复制!", "emptyCommentPrompt": "注释为空。是否将注释设置为\\"空\\"?", "confirmDeleteLinkData": "监测到链接为 {item}!确认要删除这个链接的信息吗?所有的对话内容和注释都将被删除!", "confirmCurrentURL": "监测到链接为 {item}!确认吗?", "commentSetToEmpty": "注释已设置为\\"空\\"", "enterCommentFirst": "请先输入注释内容", "commentSaved": "注释已保存", "conversationDataDeleted": "对话数据成功删除", "executeSearchWithQuery": "执行搜索:{item}", "searchPlaceholder": "输入搜索关键词...", "searchButton": "搜索", "searchInContent": "对话内容", "searchInComments": "用户注释", "searchInBoth": "内容与注释", "goToPrevious": "转到上一个", "goToNext": "转到下一个", "numberOfMatches": "{matches} 个匹配项", "nodeDetails": "详情", "enterComment": "请输入注释", "userCommentSave": "保存", "userCommentCancel": "取消", "userCommentClear": "清空", "openAdminPanel": "管理面板", "allCategoriesFilter": "所有分类", "conversationTitle": "标题", "actionOptions": "操作", "conversationCategory": "分类", "conversationTags": "标签" } }`; lang = JSON.parse(lang); const userLang = navigator.languages.find(l => l in lang) || 'en'; //const userLang = 'en'; //log("currentLang:",userLang); globalUserLang = userLang; return lang[userLang]; }; let globalUserLang; const currentLangPack = getLang(); //log("currentLangPack", currentLangPack); function translate(key) { return currentLangPack[key] || key; } const states = { mainButton: { isDragging: false, isMouseOver: false, isThereNavbar: false, isMouseInNavbar: false, isMainTreeBtnInNavbar: false, }, url: { isForLiveValidURL: false, isForDeletedValidURL: false, url: '' }, treeUpdate: { isDOMOperating: false, }, visualization: { contentDiv: 0, thumbnailSvg: 0, panelToggleButton: 0, }, }; const Default_RootNode_Content = "Chat Starts Here"; const DB_NAME = 'ChatTreeDB'; const CONVERSATIONS_STORE_NAME = 'conversations'; const SEARCH_HISTORY_STORE_NAME = 'searchHistory'; const USER_SETTINGS_STORE_NAME = 'userSettings'; let db; class DialogueNode { constructor(content, type) { this.content = content; this.type = type; this.uuid = generateUUID(); this.children = []; this.comment = ''; } } let conversationData = { url: null, rootNode: new DialogueNode(Default_RootNode_Content, "chatGPT"), uuid2pathMap: new Map(), uuid2nodeMap: new Map(), path2nodeMap: new Map(), bookMarked: new Map(), timestamp: Date.now(), participants: { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "💕", }, gpt: { type: "GPT-3", } } }; let GPT_Avatar_Config = { gpt4_Inner_Html: "
ChatGPT
", gpt3_Inner_Html: "
ChatGPT
", } let root, treeLayout, svg, svgThumbnail, defs, gLinks, gNodes, nodeDrag, canvasDrag, zoom, searchHistoryRecord, chatHistory = [], curMouseOnUUID = null; const waitForDomChange = (element) => { return new Promise(resolve => { const observer = new MutationObserver((mutationsList, observer) => { observer.disconnect(); resolve(); }); observer.observe(element, {childList: true, subtree: true}); }); }; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const treeOperation = { initializeChatTree: async function () { if (!states.url.isForLiveValidURL) { ButtonOperations.showUserNotification(translate("mismatchedLink")); return; } states.treeUpdate.isDOMOperating = true; let operatingURL = states.url.url; const button = document.querySelector('button.flex.w-full.items-center.gap-3.rounded-md.px-3.py-3.text-sm[data-headlessui-state=""]'); if (button) { const relativeFlexElement = button.querySelector('.relative.flex'); if (relativeFlexElement) { conversationData.participants.user.avatarHTML = relativeFlexElement.innerHTML; const imgElement = relativeFlexElement.querySelector('img.rounded-sm'); if (imgElement) { const imgSrc = imgElement.getAttribute('src'); conversationData.participants.user.avatarURL = imgSrc; } else { log('Image Element Not Found'); } } else { log('Relative Flex Element Not Found'); } const fontSemiboldElement = button.querySelector('.font-semibold'); if (fontSemiboldElement) { conversationData.participants.user.name = fontSemiboldElement.innerHTML; } else { log('Font Semibold Element Not Found'); } } else { let allDivs = DOMOperations.getAllDivs(); if (allDivs) { let userDiv = allDivs[0]; const imageDiv = userDiv.querySelector('div.flex-shrink-0.flex.flex-col.relative.items-end'); if (imageDiv) { const relativeFlexElement = imageDiv.querySelector('.relative.flex'); if (relativeFlexElement) { conversationData.participants.user.avatarHTML = relativeFlexElement.innerHTML; const imgElement = relativeFlexElement.querySelector('img.rounded-sm'); if (imgElement) { const imgSrc = imgElement.getAttribute('src'); conversationData.participants.user.avatarURL = imgSrc; } else { log('Image Element Not Found'); } } else { log('Relative Flex Element Not Found'); } } } log('Button Element Not Found'); } if (conversationData.rootNode.content.toLowerCase() === "New Chat".toLowerCase() || conversationData.rootNode.content.toLowerCase() === Default_RootNode_Content.toLowerCase() || conversationData.rootNode.content.toLowerCase() === "Chat Start Here".toLowerCase()) { try { const targetElement = document.querySelector('.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible'); if (targetElement) { let siblingElement = targetElement.parentElement.firstElementChild; while (siblingElement) { if (siblingElement.classList.contains('flex-1') && siblingElement.classList.contains('text-ellipsis') && siblingElement.classList.contains('max-h-5') && siblingElement.classList.contains('overflow-hidden') && siblingElement.classList.contains('break-all') && siblingElement.classList.contains('relative')) { log(siblingElement.textContent || siblingElement.innerText); conversationData.rootNode.content = siblingElement.textContent } siblingElement = siblingElement.nextElementSibling; } } else { log('Target element not found.'); } // async function testGetChatDetails() { // const token = await chatgpt.getAccessToken(); // chatgpt.getChatDetails(token, ['id', 'title']) // .then(data => { // log("Successfully got chat details:", data); // }) // .catch(error => { // console.error("Error in getChatDetails:", error); // }); // } // // await testGetChatDetails(); // 使用 chatgpt.js 库的 getChatData 方法获取聊天数据 // const chatData = await chatgpt.getChatData('active', 'all', 'all', 'all'); // //log(chatData); // 在控制台打印聊天数据 // conversationData.rootNode.content = chatData.title; // 如果需要,你可以在这里添加其他操作,例如将聊天数据发送到你的服务器 } catch (error) { console.error('Error getting chat data:', error); // 如果出现错误,在控制台打印错误信息 } } let gptInfoDiv = document.querySelector('.flex.flex-1.flex-grow.items-center.gap-1.px-2.py-1.text-gray-600'); //log(gptInfoDiv); if (gptInfoDiv) { conversationData.participants.gpt.type = gptInfoDiv.innerText; } log("url:", operatingURL); let result = DOMOperations.getButtonInfo(); let hasLeftButton = result.hasLeftButtonUnClicked; while (hasLeftButton) { if (states.url.url !== operatingURL || !states.treeUpdate.isDOMOperating) { log("监测到链接变换! 退出initializeChatTree 函数!"); states.treeUpdate.isDOMOperating = false; return; } let allChatDivs = DOMOperations.getAllDivs(); if (allChatDivs.length === 0) { log("监测到没有divs:退出初始化!"); states.treeUpdate.isDOMOperating = false; return; } let j = 0; for (; j < allChatDivs.length; j++) { if (result.childIndicesPath[j] !== 1) { DOMOperations.clickButtonAtDivLevel(0, j); log("clicking the ", j, "th div!"); await waitForDomChange(document.body); break; } } if (j === allChatDivs.length) { break; } result = DOMOperations.getButtonInfo(); log("result:", result); hasLeftButton = result.hasLeftButtonUnClicked; } log("Initializing Over."); await this.updateTree(operatingURL); }, initializeSubChatTree: async function (fromWhichDivClickLeft) { log("initializeSubChatTree"); let result = DOMOperations.getButtonInfo(); let allChatDivs = DOMOperations.getAllDivs(); if (allChatDivs.length === 0) { log("监测到没有divs:退出初始化!"); states.treeUpdate.isDOMOperating = false; return; } let hasLeftButton = false; for (let i = fromWhichDivClickLeft; i < allChatDivs.length; i++) { if (result.childIndicesPath[i] !== 1) { hasLeftButton = true; log("hasLeftButton === true"); break; } } while (hasLeftButton) { let j = fromWhichDivClickLeft; for (; j < allChatDivs.length; j++) { if (result.childIndicesPath[j] !== 1) { DOMOperations.clickButtonAtDivLevel(0, j); log("clicking the ", j, "th div!"); await waitForDomChange(document.body); break; } } if (j === allChatDivs.length) { break; } result = DOMOperations.getButtonInfo(); allChatDivs = DOMOperations.getAllDivs(); log("result:", result); for (let i = fromWhichDivClickLeft; i < allChatDivs.length; i++) { if (result.childIndicesPath[i] !== 1) { log("hasLeftButton === true"); hasLeftButton = true; break; } } } }, //🤖chatGPT版本 updateTree: async function (url) { let rootNodeContent = conversationData.rootNode.content ? conversationData.rootNode.content : Default_RootNode_Content; let newConversationData = { url: conversationData.url, rootNode: new DialogueNode(rootNodeContent, "chatGPT"), uuid2pathMap: new Map(), uuid2nodeMap: new Map(), path2nodeMap: new Map(), timestamp: Date.now(), participants: { user: { name: conversationData.participants.user.name, avatarURL: conversationData.participants.user.avatarURL, avatarHTML: conversationData.participants.user.avatarHTML, }, gpt: { type: conversationData.participants.gpt.type, } }, isWholeConversationBookMarked: conversationData.isWholeConversationBookMarked || false, chatTreeTags: conversationData.chatTreeTags || [], categories: conversationData.categories || [], bookMarked: conversationData.bookMarked }; let allpaths = []; log("更新树"); let operatingURL = url; if (states.url.url !== operatingURL) { log("监测到链接变换! 退出initializeChatTree 函数!"); return; } let allChatDivs = DOMOperations.getAllDivs(); let hasRightButtonUnClicked = true; let curRootDivChoice = 0; while (hasRightButtonUnClicked) { if (states.url.url !== operatingURL || !states.treeUpdate.isDOMOperating) { log("监测到链接变换! 退出updateTree 函数!"); states.treeUpdate.isDOMOperating = false; return; } hasRightButtonUnClicked = false; allChatDivs = DOMOperations.getAllDivs(); if (allChatDivs.length === 0) { log("监测到没有divs:退出更新!"); states.treeUpdate.isDOMOperating = false; return; } let result = DOMOperations.getButtonInfo(); for (let i = curRootDivChoice; i < allChatDivs.length; i++) { if (states.url.url !== operatingURL || !states.treeUpdate.isDOMOperating) { log("监测到链接变换! 退出initializeChatTree 函数!"); states.treeUpdate.isDOMOperating = false; return; } let contentResult = DOMOperations.getTextContent(allChatDivs[i], i); let path = result.childIndicesPath.slice(0, i + 1); // log("path", path, "content:", contentResult); // let isExisting = conversationData.path2nodeMap.get(arrayToKey(path)); // if (isExisting) { // log("isExisting!", isExisting); // log("contentResult.contentText", contentResult.contentText); // if (isExisting.content === contentResult.contentText) { // allpaths.push(arrayToKey(path)); // continue; // } else { // isExisting.content = contentResult.contentText // allpaths.push(arrayToKey(path)); // continue; // } // } let father = i > 0 ? newConversationData.path2nodeMap.get(arrayToKey(result.childIndicesPath.slice(0, i))) : newConversationData.rootNode; let newDialogueNode = new DialogueNode(contentResult.contentText, contentResult.userType); log("father :", father); father.children.push(newDialogueNode); log("newDialogueNode:", newDialogueNode); log("dataRenewing:", newConversationData); newConversationData.uuid2pathMap.set(newDialogueNode.uuid, path); newConversationData.uuid2nodeMap.set(newDialogueNode.uuid, newDialogueNode); newConversationData.path2nodeMap.set(arrayToKey(path), newDialogueNode); log("father:", father); allpaths.push(arrayToKey(path)); } let curRootChoice = allChatDivs.length - 1; for (; curRootChoice >= 0; curRootChoice--) { // if (url !== states.url.url) { // log("发现url变化! 退出更新树"); // states.treeUpdate.isDOMOperating = false; // return; // } if (result.childIndicesPath[curRootChoice] !== result.childrenCountPath[curRootChoice]) { await DOMOperations.clickButtonAtDivLevel(1, curRootChoice); let ableToContinue = false; while (!ableToContinue) { await sleep(50); let result1 = DOMOperations.getButtonInfo(); let childIndecices = result1.childIndicesPath; if (childIndecices.length !== result.childIndicesPath.length) { ableToContinue = true; break; } for (let i = 0; i < childIndecices.length; i++) { if (childIndecices[i] !== result.childIndicesPath[i]) { ableToContinue = true; break; } } } curRootDivChoice = curRootChoice; hasRightButtonUnClicked = true; //if(allChatDivs.length -1 > curRootChoice){ await treeOperation.initializeSubChatTree(curRootChoice + 1); //} break; } } if (!hasRightButtonUnClicked) { log("所有的节点都加进来了!") log("准备保存的data:", newConversationData); if (states.url.url !== operatingURL || window.location.href !== operatingURL) { log("监测到链接变化, 停止更新!"); states.treeUpdate.isDOMOperating = false; return; } try { await dbOperations.saveConversationsData(newConversationData); await dbOperations.loadConversationsData(operatingURL); } catch (error) { console.error("Error:", error); } log("重新读取到的data:", newConversationData); break; } } states.treeUpdate.isDOMOperating = false; dbOperations.initConversationData() .then(information => { controlPanelKit.renderConversations(chatHistory); }) .catch(error => console.error(error)); }, jumpToDialogueItem: async function (uuid) { if (!states.url.isForLiveValidURL) { return; } if (!uuid || !conversationData.uuid2pathMap.get(uuid) || uuid === conversationData.rootNode.uuid) { return; } let path = conversationData.uuid2pathMap.get(uuid); if (!path) { log("没有从map中获取路径! 请检查!") return; } let result = DOMOperations.getButtonInfo(); log("path:", path); let i = 0; while (i < path.length) { result = DOMOperations.getButtonInfo(); if (path[i] === result.childIndicesPath[i]) { i++; continue; } if (path[i] < result.childIndicesPath[i]) { DOMOperations.clickButtonAtDivLevel(0, i); log("path:", path); result = DOMOperations.getButtonInfo(); await sleep(200); continue; } if (path[i] > result.childIndicesPath[i]) { DOMOperations.clickButtonAtDivLevel(1, i); log("path:", path); result = DOMOperations.getButtonInfo(); await sleep(200); continue; } } let j = 0; for (; j < path.length; j++) { if (path[j] !== result.childIndicesPath[j]) { log("在:", j, "没有成功转到!"); log(path, result.childIndicesPath); break; } } sleep(200); if (j === path.length) { let allDivs = DOMOperations.getAllDivs(); log("path", path, "result.childIndicesPath", result.childIndicesPath, "allDivs[path.length - 1]", allDivs[path.length - 1]); function highlightDiv1() { allDivs[path.length - 1].classList.add('highlight'); setTimeout(() => { allDivs[path.length - 1].classList.remove('highlight'); }, 4000); } function highlightDiv2() { allDivs[path.length - 1].classList.add('highlightt'); setTimeout(() => { allDivs[path.length - 1].classList.remove('highlightt'); }, 4000); } function blinkText() { allDivs[path.length - 1].classList.add('blinkText'); setTimeout(() => { allDivs[path.length - 1].classList.remove('blinkText'); }, 5000); } allDivs[path.length - 1].scrollIntoView({behavior: 'smooth', block: 'center', inline: 'start'}); const htmlClass = document.documentElement.getAttribute('class'); if (htmlClass && htmlClass.includes('dark')) { highlightDiv1(); } else { highlightDiv2(); } setTimeout(() => { allDivs[path.length - 1].scrollIntoView({behavior: 'smooth', block: 'center', inline: 'start'}); const htmlClass = document.documentElement.getAttribute('class'); if (htmlClass && htmlClass.includes('dark')) { highlightDiv1(); } else { highlightDiv2(); } }, 1000); log("成功转到!"); } }, }; const urlOperations = { getCurrentURL: function () { return window.location.href; }, isForLiveValidURL: function (url) { const pattern = /^https:\/\/chat\.openai\.com\/c\/[a-z0-9\-]+\/?$/; return pattern.test(url); }, isForDeletedValidURL: function (url) { const pattern = /^https:\/\/chat\.openai\.com\/chattree\/[a-z0-9\-]+\/?$/; return pattern.test(url); }, changeBetweenChattreeWithCAndOneMeansChattreeToC: function (chattreetoc, url) { function replaceFirstChattreeWithC(url) { return url.replace(/^https:\/\/chat\.openai\.com\/chattree/, "https://chat.openai.com/c"); } function replaceFirstCWithChattree(url) { return url.replace(/^https:\/\/chat\.openai\.com\/c/, "https://chat.openai.com/chattree"); } if (chattreetoc) { return replaceFirstChattreeWithC(url); } else { return replaceFirstCWithChattree(url); } }, isNonUniqueURL: function (url) { const nonUniquePatterns = [ /^https:\/\/chat\.openai\.com\/?$/, /^https:\/\/chat\.openai\.com\/\?model=.+$/ ]; return nonUniquePatterns.some(pattern => pattern.test(url)); }, getCurURLInfo: function () { let curURL = window.location.href; let validURL = this.isForLiveValidURL(curURL); let nonUniqueURL = !this.isNonUniqueURL(curURL); return { curURL: curURL, validURL: validURL, nonUniqueURL: nonUniqueURL, }; }, observeTargetChanges: function () { let lastURL = window.location.href; log("lastURL:", lastURL); if (urlOperations.isForLiveValidURL(lastURL) || urlOperations.isForDeletedValidURL(lastURL)) { log("is_anyKind_of_valid"); urlOperations.handleURLChange(lastURL); states.url.url = lastURL; } function callback(mutationsList, observer) { const currentURL = window.location.href; log("currentURL:", currentURL); if (urlOperations.isForLiveValidURL(currentURL)) { if (currentURL !== lastURL) { log("URL changed:", currentURL); lastURL = currentURL; urlOperations.handleURLChange(currentURL); states.treeUpdate.isDOMOperating = false; } else { log("Current URL:", currentURL); } } else if (urlOperations.isForDeletedValidURL(currentURL)) { log("URL changed:", currentURL, " detected. Please refresh the page."); urlOperations.handleURLChange(currentURL); } else if (urlOperations.isNonUniqueURL(currentURL)) { log("Non-unique URL:", currentURL, " detected. Please refresh the page."); urlOperations.handleURLChange(currentURL); } }; let outter_observer; let inner_observer; let curState = null; const startObserving = (target, config, callback, observer) => { if (observer) { observer.disconnect(); } observer = new MutationObserver(callback); observer.observe(target, config); log('Started observing:', target); return observer; }; window.addEventListener("resize", function () { checkCurrentState(); }); const checkCurrentState = () => { const flexDiv = document.querySelector('.flex-shrink-0.overflow-x-hidden.dark.bg-gray-900'); const htmlTag = document.querySelector('html'); if (flexDiv && curState !== 1) { log('Switched to State 1.'); curState = 1; const config = {attributes: true, attributeFilter: ['style']}; outter_observer = startObserving(htmlTag, config, (mutations) => { if (mutations.some(m => m.type === 'attributes' && m.attributeName === 'style')) { log('HTML style changed.'); checkCurrentState(); } }, outter_observer); const innerConfig = {childList: true, subtree: true}; inner_observer = startObserving(flexDiv, innerConfig, (mutations) => { if (mutations.some(m => m.type === 'childList')) { log('FlexDiv child list changed.'); callback(); checkCurrentState(); } }, inner_observer); } else if (!flexDiv && curState !== 2) { log('Switched to State 2.'); curState = 2; const config = {childList: true, subtree: true}; outter_observer = startObserving(document.body, config, (mutations) => { if (mutations.some(m => m.type === 'childList')) { log('Body child list changed.'); const conversationList = document.querySelector('#headlessui-portal-root'); if (conversationList) { log('Conversation List Detected.'); inner_observer = startObserving(conversationList, config, (mutations) => { if (mutations.some(m => m.type === 'childList')) { log('Conversation List child list changed.'); callback(); } }, inner_observer); } checkCurrentState(); } }, outter_observer); } }; checkCurrentState(); }, handleURLChange: function (url) { log("In handleURLChange, Data:", conversationData); if (urlOperations.isNonUniqueURL(url)) { states.url.isForLiveValidURL = false; states.url.isForDeletedValidURL = false; states.url.url = ''; states.treeUpdate.isDOMOperating = false; conversationData = { url: null, rootNode: new DialogueNode(Default_RootNode_Content, "chatGPT"), uuid2pathMap: new Map(), uuid2nodeMap: new Map(), path2nodeMap: new Map(), bookMarked: new Map(), timestamp: Date.now(), participants: { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "User", }, gpt: { type: "GPT-3", } } }; root = d3.hierarchy(conversationData.rootNode); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); settingsKit.refreshTree(); log("请刷新页面或者转到具有对话信息的页面从而获取正确的链接"); } else if (urlOperations.isForDeletedValidURL(url)) { states.url.isForLiveValidURL = false; states.url.isForDeletedValidURL = true; states.treeUpdate.isDOMOperating = false; states.url.url = url; document.documentElement.setAttribute('class', 'light'); // const htmlClass = document.documentElement.getAttribute('class'); // let wholeScreenDiv = document.getElementById("__next"); // 修正单词拼写 // if (wholeScreenDiv && htmlClass && htmlClass === 'black') { // wholeScreenDiv.style.background = 'rgb(51,53,65)'; // } else { // document.documentElement.className = 'light'; // 使用 className // } //rgb(51,53,65) log("this_is_delete_url", url); url = urlOperations.changeBetweenChattreeWithCAndOneMeansChattreeToC(1, url); log("delete_url_to_new_url", url); dbOperations.loadConversationsData(url).then(loadeddata => { log("Loaded data for URL:", loadeddata); let interval; interval = setInterval(() => { if (document.title === "查看模式(ChatTree提供支持): " + loadeddata.rootNode.content) { clearInterval(interval); } document.title = "查看模式(ChatTree提供支持): " + loadeddata.rootNode.content; }, 1500); conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); settingsKit.refreshTree(); }).catch(error => { console.error("Error loading data:", error); }); } else { states.url.isForLiveValidURL = true; states.url.isForDeletedValidURL = true; states.treeUpdate.isDOMOperating = false; states.url.url = url; dbOperations.loadConversationsData(url).then(loadeddata => { log("Loaded data for URL:", loadeddata); conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); settingsKit.refreshTree(); }).catch(error => { console.error("Error loading data:", error); }); } } }; //const DEFAULT_MAINSVG_BACKGROUND = 'linear-gradient(to top, rgba(51, 230, 15, 0.74) 0%, rgba(250, 0, 187, 0.74) 100%)'; const DEFAULT_MAINSVG_BACKGROUND = 'linear-gradient(to top, rgba(117, 194, 102, 0.31) 0%, rgba(206, 44, 166, 0.31) 100%)'; //'linear-gradient(to top, rgba(210, 108, 196, 0.2) 0%, rgba(205, 209, 83, 0.2) 100%)' const GlobalUserSettings = { MainTreeBtnColorSettings: {}, MainTreeBtnPositionSettings: {}, MainSVGBackground: DEFAULT_MAINSVG_BACKGROUND, }; const dbOperations = { initDatabase: function () { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onerror = event => { console.error("Detailed Error:", event); reject("Error opening database:", event); }; request.onupgradeneeded = event => { log("升级数据库!"); db = event.target.result; if (!db.objectStoreNames.contains(CONVERSATIONS_STORE_NAME)) { db.createObjectStore(CONVERSATIONS_STORE_NAME, {keyPath: "url"}); } if (!db.objectStoreNames.contains(SEARCH_HISTORY_STORE_NAME)) { db.createObjectStore(SEARCH_HISTORY_STORE_NAME, {keyPath: "id"}); const initSearchHistory = {id: 'searchHistory', records: []}; const transaction = event.target.transaction; const historyStore = transaction.objectStore(SEARCH_HISTORY_STORE_NAME); historyStore.add(initSearchHistory); } if (!db.objectStoreNames.contains(USER_SETTINGS_STORE_NAME)) { db.createObjectStore(USER_SETTINGS_STORE_NAME, {keyPath: "id"}); const transaction = event.target.transaction; const userSettingsStore = transaction.objectStore(USER_SETTINGS_STORE_NAME); const mainTreeBtnColorSettings = {id: 'mainTreeBtn', color: '#0FD126', opacity: 0.9}; userSettingsStore.add(mainTreeBtnColorSettings); const mainTreeBtnPosSettings = {id: 'mainTreeBtnPos', top: '20px', left: '20px'}; userSettingsStore.add(mainTreeBtnPosSettings); } }; request.onsuccess = event => { db = event.target.result; dbOperations.getSearchHistory() .then(records => { log('Got search history:', records); }) .catch(error => console.error(error)); dbOperations.getUserSettings('mainTreeBtn') .then(MainTreeBtnSettings => { log('Got MainTreeBtnSettings:', MainTreeBtnSettings); GlobalUserSettings.MainTreeBtnColorSettings = MainTreeBtnSettings; treeMainBtn.style.background = GlobalUserSettings.MainTreeBtnColorSettings.color; treeMainBtn.style.opacity = GlobalUserSettings.MainTreeBtnColorSettings.opacity; navMainButton.style.backgroundColor = GlobalUserSettings.MainTreeBtnColorSettings ? GlobalUserSettings.MainTreeBtnColorSettings.color : "rgb(16,209,38)"; navMainButton.style.opacity = '1'; }) .catch(error => console.error(error)); dbOperations.getUserSettings('mainTreeBtnPos') .then(mainTreeBtnPos => { log('Got mainTreeBtnPosSettings:', mainTreeBtnPos); GlobalUserSettings.MainTreeBtnPositionSettings = mainTreeBtnPos; treeMainBtn.style.top = GlobalUserSettings.MainTreeBtnPositionSettings.top; treeMainBtn.style.left = GlobalUserSettings.MainTreeBtnPositionSettings.left; if (mainTreeBtnPos.isInNavbar === true) { navMainButton.style.display = 'block'; treeMainBtn.style.display = 'none'; states.mainButton.isMainTreeBtnInNavbar = true; } }) .catch(error => console.error(error)); dbOperations.getUserSettings('mainSVG') .then(mainSVGBackground => { log('Got mainTreeBtnPosSettings:', mainSVGBackground); if (mainSVGBackground && mainSVGBackground.background) { GlobalUserSettings.MainSVGBackground = mainSVGBackground.background; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { log("get mainSvg:", mainSvg); mainSvg.style.background = mainSVGBackground.background.toString(); log("get mainSVGBackground:", mainSVGBackground); } } }) .catch(error => console.error(error)); dbOperations.initConversationData() .then(information => { log("information:", information); log('initConversationData:', chatHistory); controlPanelKit.init(); controlPanelKit.renderConversations(chatHistory); controlPanelKit.updateCategorySelect(); filteredConversations = chatHistory; }) .catch(error => console.error(error)); resolve(); }; }); }, initConversationData: function () { if (!db) { return; } return new Promise((resolve, reject) => { let information = { conversations: [] }; const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); // 获取现有的 "conversations_information" 对象(如果存在) const getRequest = objectStore.get("conversations_information"); getRequest.onsuccess = event => { if (getRequest.result) { objectStore.delete("conversations_information"); } // 现在,我们可以遍历对象存储中的每个对象 objectStore.openCursor().onsuccess = event => { const cursor = event.target.result; if (cursor) { const url = cursor.value.url; if (url && !url.includes("conversations_information")) { if (cursor.value.rootNode.children.length === 0) { dbOperations.deleteConversationData(url) .then(log("a conversation is deleted!")) .catch(error => { console.error(error); }); } else { const id = url.split('/').pop(); const safeId = id.replace(/-/g, ''); information.conversations.push({ id: safeId, topic: cursor.value.rootNode.content, link: url, categories: cursor.value.categories || [], // 新增字段 chatTreeTags: cursor.value.chatTreeTags || [], // 新增字段 isWholeConversationBookMarked: cursor.value.isWholeConversationBookMarked || false, timestamp: cursor.value.timestamp }); } } cursor.continue(); } else { const putRequest = objectStore.put({ url: "conversations_information", data: information }); putRequest.onsuccess = event => { log("Information updated successfully."); resolve(information); }; putRequest.onerror = event => { console.error("Failed to update information.", event.target.error); reject(event.target.error); }; } chatHistory = information.conversations; }; }; getRequest.onerror = event => { console.error("Failed to get information.", event.target.error); reject(event.target.error); }; }); }, changeWholeConversationBookMarked: function (url, shouldBeBookMarked) { if (!url || !urlOperations.isForLiveValidURL(url)) { return; } return new Promise((resolve, reject) => { if (!db) { console.error("加载数据:Database has not been initialized."); return; } if (!url) { reject("加载数据:No URL key specified."); return; } const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const request = objectStore.get(url); request.onsuccess = event => { let result = event.target.result; result.isWholeConversationBookMarked = shouldBeBookMarked; dbOperations.saveConversationsData(result) .then(() => dbOperations.initConversationData()) .then(information => { //controlPanelKit.updateCategorySelect(); }) .catch(error => { console.error(error); }); }; request.onerror = event => reject("Error loading data:", event.target.errorCode); }); }, addOrDeleteTagOrClassToURL: async function (url, isTag, value, isAdd) { log("In DBOper:", "url:", url, "isTag:", isTag, "value:", value, "isAdd:", isAdd); if (!url || !urlOperations.isForLiveValidURL(url)) { return; } return new Promise((resolve, reject) => { if (!db) { console.error("加载数据:Database has not been initialized."); return; } if (!url) { reject("加载数据:No URL key specified."); return; } const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const request = objectStore.get(url); request.onsuccess = event => { let result = event.target.result; log("result:", result); if (isAdd) { if (!result.chatTreeTags) { result.chatTreeTags = []; } if (!result.categories) { result.categories = []; } if (isTag && !result.chatTreeTags.includes(value)) { result.chatTreeTags.push(value); log("added!"); } else if (!isTag && !result.categories.includes(value)) { result.categories.push(value); log("added!"); } } else { if (isTag && result.chatTreeTags.includes(value)) { result.chatTreeTags = result.chatTreeTags.filter(tag => tag != value); log("deleted!"); } else if (!isTag && result.categories.includes(value)) { result.categories = result.categories.filter(tag => tag != value); log("deleted!"); } } dbOperations.saveConversationsData(result) .then(() => dbOperations.initConversationData()) .then(information => { controlPanelKit.updateCategorySelect(); }) .catch(error => { console.error(error); }); }; request.onerror = event => reject("Error loading data:", event.target.errorCode); }); }, saveConversationsData: function (data) { if (data.url === null) { log("保存数据:No URL key specified."); return; } return new Promise((resolve, reject) => { if (!db) { console.error("保存数据:Database has not been initialized."); return; } if (!data.url) { reject("保存数据:No URL key specified."); return; } data.timestamp = Date.now(); const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const request = objectStore.put(data); request.onsuccess = () => resolve(); request.onerror = event => reject("Error saving data:", event.target.errorCode); }); }, getUserSettings: function (key) { return new Promise((resolve, reject) => { const transaction = db.transaction([USER_SETTINGS_STORE_NAME], 'readonly'); const store = transaction.objectStore(USER_SETTINGS_STORE_NAME); const request = store.get(key); request.onsuccess = function (event) { const settings = event.target.result; log(settings); resolve(settings); }; request.onerror = function (event) { console.error('读取设置失败', event.target.errorCode); reject(event.target.errorCode); }; }); }, updateUserSettings: function (newSettings) { return new Promise((resolve, reject) => { const transaction = db.transaction([USER_SETTINGS_STORE_NAME], 'readwrite'); const store = transaction.objectStore(USER_SETTINGS_STORE_NAME); const request = store.put(newSettings); request.onsuccess = function (event) { log('设置更新成功'); resolve(); }; request.onerror = function (event) { console.error('更新设置失败', event.target.errorCode); reject(event.target.errorCode); }; }); }, changeConversationDataTopic: async function (url, newTopic) { if (!url || !urlOperations.isForLiveValidURL(url)) { return; } return new Promise((resolve, reject) => { if (!db) { console.error("加载数据:Database has not been initialized."); return; } if (!url) { reject("加载数据:No URL key specified."); return; } const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const request = objectStore.get(url); request.onsuccess = event => { let result = event.target.result; result.rootNode.content = newTopic; dbOperations.saveConversationsData(result) .then(() => dbOperations.initConversationData()) .then(information => { controlPanelKit.updateCategorySelect(); }) .catch(error => { console.error(error); }); }; request.onerror = event => reject("Error loading data:", event.target.errorCode); }); }, loadConversationsData: function (url) { if (!url || (!urlOperations.isForLiveValidURL(url)) && (!urlOperations.isForDeletedValidURL(url))) { return; } return new Promise((resolve, reject) => { if (!db) { console.error("加载数据:Database has not been initialized."); return; } if (!url) { reject("加载数据:No URL key specified."); return; } const transaction = db.transaction([CONVERSATIONS_STORE_NAME], "readwrite"); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const request = objectStore.get(url); request.onsuccess = event => { let result = event.target.result; log(" resultOfRequest:", result); if (!result) { let conversationData = { url: url, rootNode: new DialogueNode(Default_RootNode_Content, "chatGPT"), uuid2pathMap: new Map(), uuid2nodeMap: new Map(), path2nodeMap: new Map(), bookMarked: new Map(), timestamp: Date.now(), participants: { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "User", }, gpt: { type: "GPT-3", } } }; conversationData.uuid2nodeMap.set(conversationData.rootNode.uuid, conversationData.rootNode); conversationData.uuid2pathMap.set(conversationData.rootNode.uuid, []); conversationData.path2nodeMap.set('', conversationData.rootNode); log("在load中: data", conversationData); resolve(conversationData); // const addRequest = objectStore.add(conversationData); // addRequest.onsuccess = () => { // log("返回data:", conversationData); // resolve(conversationData); // } // addRequest.onerror = event => reject("Error creating new data:", event.target.errorCode); } else { log("dataExisting!"); conversationData = result; if (!conversationData.bookMarked) { conversationData.bookMarked = new Map(); } if (!conversationData.participants) { conversationData.participants = { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "User", }, gpt: { type: "GPT-3", } } } root = d3.hierarchy(conversationData.rootNode); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); settingsKit.refreshTree(); resolve(result); } }; request.onerror = event => reject("Error loading data:", event.target.errorCode); }); }, deleteConversationData: function (url) { return new Promise((resolve, reject) => { if (!url || !urlOperations.isForLiveValidURL(url)) { reject('Invalid URL'); return; } if (!db) { console.error('删除数据: Database has not been initialized.'); reject('Database not initialized'); return; } const transaction = db.transaction([CONVERSATIONS_STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(CONVERSATIONS_STORE_NAME); const deleteRequest = objectStore.delete(url); deleteRequest.onsuccess = () => { log(`Data for URL ${url} has been deleted.`); resolve(); }; deleteRequest.onerror = event => { console.error('Error deleting data:', event.target.errorCode); reject(event.target.errorCode); }; }); }, addSearchRecord: function (records) { return new Promise((resolve, reject) => { const transaction = db.transaction([SEARCH_HISTORY_STORE_NAME], 'readwrite'); const store = transaction.objectStore(SEARCH_HISTORY_STORE_NAME); const request = store.put({id: 'searchRecords', records: records}); request.onsuccess = () => { log('搜索记录已成功更新'); resolve(); }; request.onerror = () => { console.error('更新搜索记录时出错'); reject(new Error('更新搜索记录时出错')); }; }); }, getSearchHistory: function () { return new Promise((resolve, reject) => { const transaction = db.transaction([SEARCH_HISTORY_STORE_NAME], 'readonly'); const store = transaction.objectStore(SEARCH_HISTORY_STORE_NAME); const request = store.get('searchRecords'); request.onsuccess = event => { const records = event.target.result?.records || []; log('搜索历史记录:', records); searchHistoryRecord = records; resolve(records); }; request.onerror = event => { console.error('获取搜索历史记录失败'); reject(new Error('获取搜索历史记录失败')); }; }); } }; window.addEventListener('DOMContentLoaded', (event) => { log('DOM fully loaded and parsed'); // 在这里调用你的函数 let timeout; timeout = setTimeout(() => DOMOperations.setNavBarDiv(), 1000); }); const titleDiv = document.createElement('div'); const title = document.createElement('h3'); const stickyDiv = document.createElement('div'); const navPanelButton = document.createElement('button'); const navMainButton = document.createElement('button'); const DOMOperations = { setNavBarDiv: function () { let HistoryDiv = document.querySelector('.flex-col.flex-1.transition-opacity.duration-500.-mr-2.pr-2.overflow-y-auto'); let NoHistoryDiv = document.querySelector('.absolute.left-0.top-14.z-20.overflow-hidden.transition-all.duration-500.invisible.max-h-0'); // let navbarHTML = ` //

ChatGPT_ChatTree

//
// //
// ` //let navbarDiv = document.createElement('div'); // navbarDiv.innerHTML = navbarHTML; navPanelButton.addEventListener('click', controlPanelKit.toggleHistoryPanel); // navbarDiv.appendChild(titleDiv); // navbarDiv.appendChild(navPanelButton); log('Searching for elements...'); let isAdded = false; if (HistoryDiv || NoHistoryDiv) { log('Elements found:', HistoryDiv, NoHistoryDiv); if (HistoryDiv && HistoryDiv.parentNode) { HistoryDiv.parentNode.insertBefore(stickyDiv, HistoryDiv); navPanelButton.parentNode.insertBefore(navMainButton, navPanelButton); HistoryDiv.parentNode.insertBefore(titleDiv, stickyDiv); } isAdded = true; } else { log('Elements not found'); treeMainBtn.style.display = 'block'; states.mainButton.isMainTreeBtnInNavbar = false; } return isAdded; }, getAllDivs: function () { return document.querySelectorAll('div[data-testid^="conversation-turn-"]'); }, clickButtonAtDivLevel: function (buttonIndex, divLevel = -1,) { let allChatDivs = DOMOperations.getAllDivs(); if (divLevel >= allChatDivs.length) { return; } let conversationDiv = allChatDivs[divLevel]; const buttonInfoDiv = conversationDiv.querySelector('.text-xs.flex.items-center'); let buttons = buttonInfoDiv.querySelectorAll("button"); if (buttons) { log("In clickButtonAtDivLevel", "divLevel: ", divLevel, "buttons:", buttons); buttons[buttonIndex].click(); } else { log("TargetDiv Not found!"); } }, getButtonInfo: function () { log("In getButtonInfo!"); let hasRightButtonUnClicked = false; let hasLeftButtonUnClicked = false; let allChatDivs = DOMOperations.getAllDivs(); let childIndicesPath = []; let childrenCountPath = []; for (let i = 0; i < allChatDivs.length; i++) { let div = allChatDivs[i]; //text-xs flex items-center justify-center gap-1 absolute left-0 top-2 -ml-4 -translate-x-full gizmo:top-1 gizmo:-ml-6 group:hover-visible visible //text-xs flex items-center justify-center gap-1 self-center pt-2 visible const buttonInfoDiv = div.querySelector('.text-xs.flex.items-center'); if (buttonInfoDiv) { let span = buttonInfoDiv.querySelector("span"); if (span) { let spanText = span.innerText || span.textContent; let match = spanText.match(/(\d+) \/ (\d+)/); if (match) { let currentVersion = parseInt(match[1], 10); let totalVersions = parseInt(match[2], 10); childIndicesPath.push(currentVersion); childrenCountPath.push(totalVersions); } else { childIndicesPath.push(1); childrenCountPath.push(1); } } else { childIndicesPath.push(1); childrenCountPath.push(1); } } else { log("!didNot find the targetDiv of buttons!"); childIndicesPath.push(1); childrenCountPath.push(1); } } log("divlength:", allChatDivs.length, "childIndices:", childIndicesPath, "childrenCount", childrenCountPath); for (let i = 0; i < childrenCountPath.length; i++) { if (childIndicesPath[i] !== 1) hasLeftButtonUnClicked = true; if (childIndicesPath[i] !== childrenCountPath[i]) hasRightButtonUnClicked = true; } return { childIndicesPath: childIndicesPath, childrenCountPath: childrenCountPath, hasRightButtonUnClicked: hasRightButtonUnClicked, hasLeftButtonUnClicked: hasLeftButtonUnClicked }; }, getTextContent: function (div, i) { let isUser = null; let isGPT = null; //isUser = div.querySelector('div.flex.flex-col.items-start.gap-3.overflow-x-auto.whitespace-pre-wrap.break-words'); isGPT = div.querySelector(".markdown.prose"); if (isGPT) { isGPT = div.querySelector(".markdown.prose"); } else { isUser = div.querySelector('div.flex.flex-col.items-start.gap-3.overflow-x-auto.whitespace-pre-wrap.break-words'); } let userType = isUser ? "用户" : "chatGPT"; if (!userType) { userType = i % 2 ? "chatGPT" : "用户"; } let contentText; if (userType === "用户") if (isUser) contentText = isUser.innerText; else contentText = ''; else { if (isGPT) contentText = isGPT.innerHTML; else contentText = ''; } return { userType: userType, contentText: contentText } } }; function arrayToKey(arr) { return arr.join('|'); } function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function start() { ButtonOperations.showUserNotification(translate("chatTreeRunning")); dbOperations.initDatabase().then(() => { if (!db) { ButtonOperations.showUserNotification(translate("noDatabaseAndCreationFailed")); return; } log("Database initialized successfully."); log("database:", db); urlOperations.observeTargetChanges(); }).catch(error => { console.error("Error initializing database:", error); }); } function toggleSvgShow(isShowSvgButton = 0) { let mainSvg = document.getElementById("mainSvg") let mainSvgDiv = document.getElementById("mainSvgDiv") let thumbnail = document.getElementById("thumbnailSvg") let isMainSvgsDisplayed = mainSvgDiv.style.display; if (states.visualization.thumbnailSvg === 0) { states.visualization.thumbnailSvg = thumbnail.getAttribute("visibility"); states.visualization.contentDiv = contentDiv.style.display; states.visualization.panelToggleButton = panelToggleButton.style.display; states.visualization.treeMainBtn = treeMainBtn.style.display; } log("states.visualization.thumbnailSvg", states.visualization.thumbnailSvg); if (isMainSvgsDisplayed === 'block') { managePanel.style.display = 'none'; mainSvgDiv.style.display = "none"; settingsContainer.style.display = "none"; searchContainer.style.display = 'none'; mainSvg.setAttribute("visibility", "hidden"); rightMiddleContainer.style.display = 'none'; if (states.visualization.thumbnailSvg === "visible") { thumbnail.setAttribute("visibility", "hidden"); } if (states.visualization.contentDiv === 'block') { contentDiv.style.display = 'none'; } if (states.mainButton.isMainTreeBtnInNavbar === true) { treeMainBtn.style.display = 'none'; } panelToggleButton.style.display = 'none'; commentForm.style.display = 'none'; document.documentElement.style.overflow = ''; } else if (isShowSvgButton) { mainSvgDiv.style.display = "block"; settingsContainer.style.display = "block"; searchContainer.style.display = 'flex'; mainSvg.setAttribute("visibility", "visible"); rightMiddleContainer.style.display = 'block'; treeMainBtn.style.display = 'block'; if (states.visualization.contentDiv === 'block') { contentDiv.style.display = 'block'; } if (states.visualization.thumbnailSvg === "visible") { thumbnail.setAttribute("visibility", "visible"); } panelToggleButton.style.display = 'block'; states.visualization.thumbnailSvg = 0; states.visualization.contentDiv = 0; document.documentElement.style.overflow = 'hidden'; } } window.addEventListener('keydown', function (event) { if (event.key === 'Escape' || event.keyCode === 27) { log('Esc key was pressed'); toggleSvgShow(); } }); let treeMainBtn = document.createElement("button"); let isDragging = false; let isMouseOver = false; let offsetX, offsetY; let menu; let mainBtnColorPicker, mainBtnOpacityPicker; const ButtonOperations = { createButton: function () { treeMainBtn.style.display = 'none'; treeMainBtn.className = 'main-button' treeMainBtn.id = "chatTreeBtn"; treeMainBtn.innerHTML = "🌳ChatTree🌳"; treeMainBtn.style.position = "fixed"; //treeMainBtn.style.right = '20px'; //treeMainBtn.style.top = '20px'; try { treeMainBtn.style.left = GlobalUserSettings.MainTreeBtnPositionSettings.left ? GlobalUserSettings.MainTreeBtnPositionSettings.left : '20px'; treeMainBtn.style.top = GlobalUserSettings.MainTreeBtnPositionSettings.top ? GlobalUserSettings.MainTreeBtnPositionSettings.top : '20px'; } catch (e) { treeMainBtn.style.right = '30%'; treeMainBtn.style.top = '20px'; } treeMainBtn.style.zIndex = "9999"; treeMainBtn.style.resize = "both"; treeMainBtn.style.width = "150px"; treeMainBtn.style.color = "white"; treeMainBtn.style.height = "30px"; treeMainBtn.style.backgroundColor = GlobalUserSettings.MainTreeBtnColorSettings ? GlobalUserSettings.MainTreeBtnColorSettings.color : "rgb(16,209,38)"; treeMainBtn.style.opacity = GlobalUserSettings.MainTreeBtnColorSettings ? GlobalUserSettings.MainTreeBtnColorSettings.opacity : "0.9"; treeMainBtn.style.borderRadius = "12px"; document.body.appendChild(treeMainBtn); treeMainBtn.style.display = 'block'; treeMainBtn.addEventListener("mouseenter", () => { ButtonOperations.showMenu(0) }); treeMainBtn.addEventListener("mouseleave", ButtonOperations.hideMenuIfNotOver); treeMainBtn.addEventListener("mousedown", ButtonOperations.onMouseDown); treeMainBtn.addEventListener("click", ButtonOperations.onClick); titleDiv.classList.add('sticky', 'top-0', 'z-[16]'); titleDiv.setAttribute('data-projection-id', '6'); titleDiv.style.opacity = '1'; title.classList.add('h-9', 'pb-2', 'pt-3', 'px-3', 'gizmo:px-2', 'text-xs', 'text-gray-500', 'font-medium', 'text-ellipsis', 'overflow-hidden', 'break-all', 'bg-gray-50', 'gizmo:bg-white', 'dark:bg-gray-900', 'gizmo:dark:bg-black', 'gizmo:text-gizmo-gray-600'); title.textContent = '🌳ChatGPT • ChatTree🌳'; titleDiv.appendChild(title); stickyDiv.style.flexDirection = 'column'; stickyDiv.style.alignItems = 'stretch'; stickyDiv.style.display = 'flex'; navPanelButton.style.display = 'block'; navPanelButton.style.borderRadius = '12px'; navPanelButton.style.opacity = '0.9'; navPanelButton.style.background = 'linear-gradient(to right, rgb(0, 123, 255), rgb(0, 198, 255))'; navPanelButton.style.color = 'white'; navPanelButton.style.padding = '4px'; navPanelButton.style.fontWeight = 'bold'; navPanelButton.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 3px 5px'; navPanelButton.style.margin = '6px'; navPanelButton.textContent = translate("openAdminPanel"); navPanelButton.style.height = '30px'; stickyDiv.appendChild(navPanelButton); navMainButton.classList.add('main-button'); navMainButton.className = 'main-button'; navMainButton.id = 'navChatTreeBtn'; navMainButton.style.display = 'block'; navMainButton.style.zIndex = '9999'; navMainButton.style.resize = 'both'; navMainButton.style.height = '30px'; navMainButton.style.borderRadius = '12px'; navMainButton.style.margin = '6px'; navMainButton.style.right = 'auto'; navMainButton.style.bottom = 'auto'; navMainButton.style.color = 'white'; navMainButton.style.padding = '4px'; navMainButton.style.fontWeight = 'bold'; navMainButton.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 3px 5px'; navMainButton.style.display = 'none'; navMainButton.textContent = '🌳ChatTree🌳'; navMainButton.addEventListener("mouseenter", () => { ButtonOperations.showMenu(1) }); navMainButton.addEventListener("mousedown", ButtonOperations.onMouseDown); navMainButton.addEventListener("mouseleave", ButtonOperations.hideMenuIfNotOver); }, // handleNavbarMouseEvent: function (event) { // states.mainButton.isMouseInNavbar = event.type === 'mouseenter'; // }, onMouseDown: function (e) { // let navbar = document.querySelector('.flex.h-full.min-h-0.flex-col'); // states.mainButton.isThereNavbar = !!navbar; // if(navbar){ // navbar.addEventListener('mouseenter', ButtonOperations.handleNavbarMouseEvent); // navbar.addEventListener('mouseleave', ButtonOperations.handleNavbarMouseEvent); // } if (e.button !== 0) return; let rect = treeMainBtn.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; isDragging = true; window.addEventListener("mousemove", ButtonOperations.onMouseMove); window.addEventListener("mouseup", ButtonOperations.onMouseUp); }, onMouseMove: function (e) { if (!isDragging) return; let navbar = document.querySelector('.flex.h-full.min-h-0.flex-col'); states.mainButton.isThereNavbar = !!navbar; if (states.mainButton.isThereNavbar) { let navbarRect = navbar.getBoundingClientRect(); let mouseX = e.clientX; let mouseY = e.clientY; states.mainButton.isMouseInNavbar = mouseX >= navbarRect.left && mouseX <= navbarRect.right && mouseY >= navbarRect.top && mouseY <= navbarRect.bottom; if (states.mainButton.isMouseInNavbar) { if (!states.mainButton.isMainTreeBtnInNavbar) { navMainButton.style.display = 'block'; treeMainBtn.style.display = 'none'; states.mainButton.isMainTreeBtnInNavbar = true; } return; } else if (states.mainButton.isMainTreeBtnInNavbar) { navMainButton.style.display = 'none'; treeMainBtn.style.display = 'block'; states.mainButton.isMainTreeBtnInNavbar = false; offsetX = 15/// - rect.left; offsetY = 5// - rect.top; } } ButtonOperations.hideMenu(); let top = e.clientY - offsetY; let left = e.clientX - offsetX; let maxWidth = window.innerWidth; let maxHeight = window.innerHeight; let elementWidth = treeMainBtn.offsetWidth; let elementHeight = treeMainBtn.offsetHeight; if (left < 0) { left = 0; } else if (left > maxWidth - elementWidth) { left = maxWidth - elementWidth; } if (top < 0) { top = 0; } else if (top > maxHeight - elementHeight) { top = maxHeight - elementHeight; } treeMainBtn.style.left = left + "px"; treeMainBtn.style.top = top + "px"; treeMainBtn.style.right = "auto"; treeMainBtn.style.bottom = "auto"; }, onMouseUp: function () { log("mouseup_in_treemainBtn!"); isDragging = false; if (states.mainButton.isMainTreeBtnInNavbar !== true) { const newSettings = { id: 'mainTreeBtnPos', left: treeMainBtn.style.left, top: treeMainBtn.style.top, isInNavbar: false }; dbOperations.updateUserSettings(newSettings).then(() => { }).catch(error => { console.error("Error saving Change:", error); }); } else { const newSettings = { id: 'mainTreeBtnPos', left: treeMainBtn.style.left, top: treeMainBtn.style.top, isInNavbar: true }; dbOperations.updateUserSettings(newSettings).then(() => { }).catch(error => { console.error("Error saving Change:", error); }); } window.removeEventListener("mousemove", ButtonOperations.onMouseMove); window.removeEventListener("mouseup", ButtonOperations.onMouseUp); }, onClick: function (e) { if (isDragging) { e.stopPropagation(); e.preventDefault(); ButtonOperations.hideMenu(); } }, showMenu: function (fromNavbar = 0) { log("mouseEnter!, fromNavbar:", fromNavbar); if (isDragging) return; isMouseOver = true; if (menu) return; menu = document.createElement("div"); let updateCurrentConversationTreeText = translate("updateCurrentConversationTree"); let adjustBackgroundColorAndOpacityText = translate("adjustBackgroundColorAndOpacity"); let toggleConversationTreeText = translate("toggleConversationTree"); function rgbToHex(rgb) { let match = rgb.match(/\d+/g); if (match) { let r = parseInt(match[0]); let g = parseInt(match[1]); let b = parseInt(match[2]); return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0'); } return '#000000'; // Return a default color if the conversion fails } let color = treeMainBtn.style.background; let hexColor = rgbToHex(color); let opacity = parseFloat(treeMainBtn.style.opacity); log("color:", hexColor, 'opacity:', opacity); menu.innerHTML = states.url.isForLiveValidURL ? ` ` : ` ` ; menu.style.position = "fixed"; menu.style.zIndex = "10000"; menu.style.backgroundColor = "transparent"; menu.style.border = "none"; document.body.appendChild(menu); ButtonOperations.positionMenu(fromNavbar); const menuOptions = menu.querySelectorAll('.menu-option'); menuOptions.forEach((el, index) => { el.style.transition = 'all 400ms ease-out'; el.style.transform = `translateX(${treeMainBtn.getBoundingClientRect().width / 3}px)`; el.style.opacity = Math.max(0.2, parseFloat(treeMainBtn.style.opacity) - 0.4); setTimeout(() => { el.style.transform = 'translateX(0)'; el.style.opacity = treeMainBtn.style.opacity; }, index * 100); }) function rgbToHsl(r, g, b) { r /= 255, g /= 255, b /= 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; } function hslToRgb(h, s, l) { let r, g, b; if (s === 0) { r = g = b = l; } else { function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } let q = l < 0.5 ? l * (1 + s) : l + s - l * s; let p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [r * 255, g * 255, b * 255]; } menuOptions.forEach(option => { option.addEventListener('mouseover', () => { const bgColor = window.getComputedStyle(treeMainBtn).backgroundColor; const rgb = bgColor.match(/[\d.]+/g).map(Number); let [h, s, l] = rgbToHsl(...rgb); l *= 0.8 const [r, g, b] = hslToRgb(h, s, l); option.style.backgroundColor = `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; }); option.addEventListener('mouseout', () => { option.style.backgroundColor = treeMainBtn.style.backgroundColor; }); }); menu.addEventListener("click", ButtonOperations.onMenuClick); menu.addEventListener("mouseenter", () => isMouseOver = true); menu.addEventListener("mouseleave", ButtonOperations.hideMenuIfNotOver); mainBtnColorPicker = document.getElementById('mainBtnColorPicker'); mainBtnOpacityPicker = document.getElementById('mainBtnOpacityPicker'); mainBtnColorPicker.addEventListener('input', ButtonOperations.onColorChange); mainBtnColorPicker.addEventListener('change', ButtonOperations.onColorChangeDone); mainBtnOpacityPicker.addEventListener('input', ButtonOperations.onOpacityChange); mainBtnOpacityPicker.addEventListener('change', ButtonOperations.onOpacityChangeDone); document.querySelectorAll('.menu-option').forEach(el => { el.style.backgroundColor = treeMainBtn.style.backgroundColor; el.style.opacity = treeMainBtn.style.opacity; }); }, hideMenuIfNotOver: function () { isMouseOver = false; setTimeout(() => { if (!isMouseOver) ButtonOperations.hideMenu(); }, 100); }, hideMenu: function () { if (menu) { document.body.removeChild(menu); menu = null; } }, onMenuClick: function (e) { if (e.target.id === 'adjustOption') { mainBtnColorPicker.style.display = 'block'; mainBtnOpacityPicker.style.display = 'inline-block'; } if (e.target.id === 'opt_updateTree') { let curURL = window.location.href; log("curURL:", curURL, "states:", states); if (curURL !== states.url.url) { if (urlOperations.isForLiveValidURL(curURL)) { if (confirm(translate("confirmCurrentURL").replace('{item}', curURL))) { urlOperations.handleURLChange(curURL); ButtonOperations.showUserNotification(translate("startUpdatingConversationTree")); } else { return; } } else { alert("Please Refresh The Page First!🔄️") return; } } log("按钮点击而开始更新树!states:", states); if (states.url.isForLiveValidURL === true && states.url.url !== '' && !states.treeUpdate.isDOMOperating) { log("由于按钮点击而开始更新树!"); let allDivs = DOMOperations.getAllDivs(); if (allDivs.length === 0) { log("没有检测到Div!请刷新页面获取对话信息!"); return; } setTimeout(() => { if (confirm("This will delete previous comments!")) { treeOperation.initializeChatTree() } }, 100); } else { log("按钮点击而开始更新树!但是条件不允许!states:", states); } } if (e.target.id === 'showSvg') { toggleSvgShow(1); } if (e.target.id === 'adjustOption' || e.target.id === 'opt_updateTree' || e.target.id === 'showSvg') ButtonOperations.showUserNotification(translate("selectedItem").replace('{item}', e.target.innerText)); }, currentMessages: 0, showUserNotification: function (text, alertOrNote = "note", duration = 3000) { const message = document.createElement("div"); const innerHtml = (alertOrNote === "note") ? `` : `
`; /*
*/ message.style.position = "fixed"; message.style.top = "0"; message.style.zIndex = "10001"; message.style.opacity = treeMainBtn.style.opacity; message.style.transform = "translateY(-100%)"; message.style.transition = "all 400ms ease-in"; message.style.marginTop = "10px"; message.style.opacity = '0'; message.innerHTML = innerHtml; log("message:", message); document.body.appendChild(message); const leftPosition = (window.innerWidth - message.offsetWidth) / 2; message.style.left = `${leftPosition}px`; let offset = this.currentMessages * 50 + 10; message.style.top = `${offset}px`; this.currentMessages++; document.body.appendChild(message); setTimeout(() => { message.style.transform = "translateY(0)"; message.style.opacity = '1'; }, 0); setTimeout(() => { message.style.transform = "translateY(-100%)"; message.style.opacity = '0'; setTimeout(() => { document.body.removeChild(message); this.currentMessages--; }, 400); }, duration); }, positionMenu: function (fromNavbar = 0) { log("in_position:,fromNavbar", fromNavbar); let rect; if (!fromNavbar) { rect = treeMainBtn.getBoundingClientRect(); } else { rect = navMainButton.getBoundingClientRect(); } log("rect:", rect); let menuLeft = fromNavbar ? rect.left + 15 : rect.left - 15; menu.style.top = `${rect.bottom}px`; menu.style.left = `${menuLeft}px`; menu.style.width = `${rect.width}px`; }, onColorChange: function (e) { const color = e.target.value; treeMainBtn.style.backgroundColor = color; navMainButton.style.backgroundColor = color; if (menu) { document.querySelectorAll('.menu-option').forEach(el => { el.style.backgroundColor = color; }); } }, onOpacityChange: function (e) { const opacity = e.target.value / 100; treeMainBtn.style.opacity = opacity.toString(); if (menu) { document.querySelectorAll('.menu-option').forEach(el => { el.style.opacity = opacity.toString(); }); } }, onOpacityChangeDone: function (e) { const opacity = e.target.value / 100; let opacity_string = opacity.toString(); const newSettings = {id: 'mainTreeBtn', color: treeMainBtn.style.backgroundColor, opacity: opacity_string}; dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onColorChangeDone: function (e) { const opacity_string = treeMainBtn.style.opacity const newSettings = {id: 'mainTreeBtn', color: treeMainBtn.style.backgroundColor, opacity: opacity_string}; dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, }; start(); ButtonOperations.createButton(); const initSvgAndGradient = { thumbnailStyles: { "position": "fixed", "bottom": "10px", "left": "10px" }, createSvg: function (selection, styles = {}, width = "100%", height = "100%", createDiv = true, svgId) { let parent = selection; if (createDiv) { parent = selection.append("div") .style("width", width) .style("height", height) .style("position", "fixed") .style("top", "0px") .style("left", "0px") .style("display", 'none') .attr("id", "mainSvgDiv") } let svgEl = parent.append("svg") .attr("width", "100%") .attr("height", "100%") .attr("visibility", "hidden") for (let style in styles) { svgEl.style(style, styles[style]); } if (svgId) { svgEl.attr("id", svgId); } return svgEl; }, createLinearGradient: function (defs, id, colorStart, colorEnd) { const gradient = defs.append("linearGradient") .attr("id", id) .attr("x1", "0%") .attr("y1", "100%") .attr("x2", "0%") .attr("y2", "0%"); gradient.append("stop") .attr("offset", "0%") .attr("stop-color", colorStart) .attr("stop-opacity", 1); gradient.append("stop") .attr("offset", "100%") .attr("stop-color", colorEnd) .attr("stop-opacity", 1); }, Visualizationinit: function (data) { if (!data) { log("data = null! return!"); } else { root = d3.hierarchy(data); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); } svg = initSvgAndGradient.createSvg(d3.select("body"), {}, "100%", "100%", true, "mainSvg"); svgThumbnail = initSvgAndGradient.createSvg(d3.select("body"), initSvgAndGradient.thumbnailStyles, "0px", "0px", false, "thumbnailSvg"); let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { mainSvg.style.background = GlobalUserSettings.MainSVGBackground ? GlobalUserSettings.MainSVGBackground : DEFAULT_MAINSVG_BACKGROUND; } defs = svg.append("defs"); initSvgAndGradient.createLinearGradient(defs, "chatgptGradient", "#34aeeb", "#0a87d8"); initSvgAndGradient.createLinearGradient(defs, "userGradient", "#ffc085", "#ff7f00"); gLinks = svg.append("g"); gNodes = svg.append("g"); window.addEventListener('wheel', function (event) { if (states.visualization.thumbnailSvg === 'visible') { setTimeout(drawMainSVG, 200); } let mainSvgDiv = document.getElementById("mainSvgDiv"); if (mainSvgDiv.style.display === 'block' && event.ctrlKey) { event.preventDefault(); // 阻止默认行为,即浏览器的缩放操作 } }, {passive: false}); window.addEventListener("resize", function () { if (treeMainBtn) { const maxX = window.innerWidth - treeMainBtn.offsetWidth; const maxY = window.innerHeight - treeMainBtn.offsetHeight; let left = parseInt(treeMainBtn.style.left); let top = parseInt(treeMainBtn.style.top); left = Math.min(maxX, Math.max(0, left)); top = Math.min(maxY, Math.max(0, top)); treeMainBtn.style.left = left + "px"; treeMainBtn.style.top = top + "px"; treeMainBtn.style.right = "auto"; treeMainBtn.style.bottom = "auto"; } let contentDiv = document.getElementById("contentDiv"); if (contentDiv) { const maxX = window.innerWidth - contentDiv.offsetWidth; const maxY = window.innerHeight - contentDiv.offsetHeight; let left = parseInt(contentDiv.style.left, 10); // 解析当前的 left 值 let top = parseInt(contentDiv.style.top, 10); // 解析当前的 top 值 // 判断是否超出窗口界限 let outOfBoundsX = left < 0 || left > maxX; let outOfBoundsY = top < 0 || top > maxY; if (outOfBoundsX || outOfBoundsY) { // 如果超过窗口界限,则重新定位到窗口右上角 contentDiv.style.left = maxX + "px"; contentDiv.style.top = "0px"; contentDiv.style.right = "auto"; contentDiv.style.bottom = "auto"; } else { // 如果没有超过界限,则保持现有位置 left = Math.min(maxX, Math.max(0, left)); top = Math.min(maxY, Math.max(0, top)); contentDiv.style.left = left + "px"; contentDiv.style.top = top + "px"; } } }); } } initSvgAndGradient.Visualizationinit(conversationData.rootNode); //dragAndZoomKits const dragAndZoomKits = { initialPageX: 0, initialPageY: 0, initialTranslateX: 0, initialTranslateY: 0, nodeDragStarted: function (d) { nodesInAndOutKit.highlightDescendantsOnDrag(d); newOperation = { type: "drag", uuid: d.data.uuid, startX: d.x, startY: d.y, } redoStack = []; }, nodeDragged: function (d, i) { const dx = d3.event.x - d.x; const dy = d3.event.y - d.y; function move(node) { node.x += dx; node.y += dy; if (node.children) { node.children.forEach(move); } let windowElem = document.getElementById(node.data.uuid + "-window"); if (windowElem) { let currentTop = parseFloat(windowElem.style.top); let currentLeft = parseFloat(windowElem.style.left); windowElem.style.top = (currentTop + dy) + 'px'; windowElem.style.left = (currentLeft + dx) + 'px'; } } move(d); drawMainSVG(); }, nodeDragEnded: function (d) { gNodes.selectAll(".node").classed("descendant-dragging", false); d3.select(this).style("cursor", "pointer"); newOperation.endX = d.x; newOperation.endY = d.y; if (newOperation.startX !== newOperation.endX || newOperation.startY !== newOperation.endY) settingsKit.performAction(newOperation); }, canvasDragStarted: function () { const currentTransform = d3.zoomTransform(svg.node()); this.initialTranslateX = currentTransform.x; this.initialTranslateY = currentTransform.y; this.initialPageX = d3.event.sourceEvent.pageX; this.initialPageY = d3.event.sourceEvent.pageY; }, canvasDragged: function () { const dx = (d3.event.sourceEvent.pageX - this.initialPageX); const dy = (d3.event.sourceEvent.pageY - this.initialPageY); const newTranslateX = this.initialTranslateX + dx; const newTranslateY = this.initialTranslateY + dy; svg.call(zoom.transform, d3.zoomIdentity.translate(newTranslateX, newTranslateY).scale(d3.zoomTransform(svg.node()).k)); drawThumbnailSVG(); }, zoomed: function () { const transform = d3.event.transform; gNodes.attr("transform", transform.toString()); gLinks.attr("transform", transform.toString()); drawThumbnailSVG(); }, init: function (svgElement) { nodeDrag = d3.drag() .on("start", dragAndZoomKits.nodeDragStarted) .on("drag", dragAndZoomKits.nodeDragged) .on("end", dragAndZoomKits.nodeDragEnded); canvasDrag = d3.drag() .on("start", this.canvasDragStarted) .on("drag", this.canvasDragged); zoom = d3.zoom() .scaleExtent([0.1, 10]) .filter(() => d3.event.ctrlKey) .on("zoom", this.zoomed); svgElement.call(zoom); svgElement.call(canvasDrag); }, } dragAndZoomKits.init(svg); function drawThumbnailSVG() { const thumbReservedSpace = 0.2; svgThumbnail.selectAll("*").remove(); const xRange = d3.extent(root.descendants(), d => d.x); const yRange = d3.extent(root.descendants(), d => d.y); const treeWidth = xRange[1] - xRange[0]; const treeHeight = yRange[1] - yRange[0]; const thumbScale = 0.2; const DEFAULTS = { THUMB_WIDTH_MIN: 270, THUMB_WIDTH_MAX: 400, THUMB_HEIGHT_MIN: 250, THUMB_HEIGHT_MAX: 1000, }; let thumbWidth = Math.max(DEFAULTS.THUMB_WIDTH_MIN, Math.min(DEFAULTS.THUMB_WIDTH_MAX, treeWidth * thumbScale * (1 + 1.2 * thumbReservedSpace))); let thumbHeight = Math.max(DEFAULTS.THUMB_HEIGHT_MIN, Math.min(DEFAULTS.THUMB_HEIGHT_MAX, treeHeight * (thumbScale) * (1 + 1 * thumbReservedSpace))); const xOffset = Math.min(Math.max(treeWidth * thumbScale * 0.8 * thumbReservedSpace, 0.9 * thumbWidth), 0.1 * thumbWidth); const yOffset = thumbHeight * 0.05; svgThumbnail .attr("width", thumbWidth) .attr("height", thumbHeight) .style("border-radius", "10px") svgThumbnail.append("rect") .attr("width", thumbWidth) .attr("height", thumbHeight) .attr("rx", 10) .attr("ry", 10) .style("fill", "none") .style("stroke", "rgba(211,211,211,1)") .style("stroke-width", 5); svgThumbnail.selectAll(".link") .data(root.links()) .enter().append("line") .attr("class", "link") .attr("x1", d => (d.source.x - xRange[0]) * thumbScale + xOffset) .attr("y1", d => (d.source.y - yRange[0]) * thumbScale + yOffset) .attr("x2", d => (d.target.x - xRange[0]) * thumbScale + xOffset) .attr("y2", d => (d.target.y - yRange[0]) * thumbScale + yOffset) .style("stroke", "#aaa"); svgThumbnail.selectAll(".node") .data(root.descendants()) .enter().append("circle") .attr("class", "node") .attr("cx", d => (d.x - xRange[0]) * thumbScale + xOffset) .attr("cy", d => (d.y - yRange[0]) * thumbScale + yOffset) .attr("r", 2) .style("fill", d => { return d.data.type === "用户" ? "url(#userGradient)" : "url(#chatgptGradient)"; }); const transform = d3.zoomTransform(svg.node()); const currentZoom = transform.k; const currentTranslateX = transform.x; const currentTranslateY = transform.y; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; root.descendants().forEach(node => { const adjustedX = node.x * currentZoom + currentTranslateX; const adjustedY = node.y * currentZoom + currentTranslateY; const svgRect = svg.node().getBoundingClientRect(); const screenX = adjustedX + svgRect.left; const screenY = adjustedY + svgRect.top; const isVisible = (screenX >= -6 && screenX <= window.innerWidth + 6) && (screenY >= -6 && screenY <= window.innerHeight + 6); if (isVisible) { if (node.x < minX) minX = node.x; if (node.x > maxX) maxX = node.x; if (node.y < minY) minY = node.y; if (node.y > maxY) maxY = node.y; } }); if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) { return; } const rectX = (minX - xRange[0]) * thumbScale + xOffset; const rectY = (minY - yRange[0]) * thumbScale + yOffset; const rectWidth = (maxX - minX) * thumbScale; const rectHeight = (maxY - minY) * thumbScale; svgThumbnail.append("rect") .attr("x", rectX - 6) .attr("y", rectY - 6) .attr("width", rectWidth + 12) .attr("height", rectHeight + 12) .attr("rx", 10) .attr("ry", 10) .style("fill", "rgba(211,211,211,0.3)") .style("stroke", "rgba(80, 80, 80, 1)") .style("stroke-width", 2); } function drawMainSVG() { const links = gLinks.selectAll(".link") .data(root.links(), d => d.target.data.uuid); links.enter() .append("path") .attr("class", "link") .attr("fill", "none") .attr("stroke", "white") .attr("stroke-width", "2") .merge(links) .attr("d", d => { return `M ${d.source.x} ${d.source.y} C ${(d.source.x + d.target.x) / 2} ${d.source.y}, ${(d.source.x + d.target.x) / 2} ${d.target.y}, ${d.target.x} ${d.target.y}`; }); links.exit().remove(); const nodes = gNodes.selectAll(".node") .data(root.descendants(), d => d.data.uuid); const nodesEnter = nodes.enter() .append("g") .attr("class", "node") .attr("transform", d => `translate(${d.x},${d.y})`) .call(nodeDrag); nodesEnter.append("circle") .attr("r", 10) .attr("class", function (d) { return d.data.type.toLowerCase(); }); nodesEnter.append("text") .text("✅") .style("display", "none") .attr("text-anchor", "middle") .attr("dy", ".35em") .style("font-size", "12px"); d3.selectAll(".node circle.chatgpt") .style("fill", "url(#chatgptGradient)"); d3.selectAll(".node circle.用户") .style("fill", "url(#userGradient)"); nodesEnter.append("text") .attr("class", "emoji-tag") .attr("x", d => +0) .attr("y", d => -4) .text("📌") .attr("visibility", "hidden") .filter(d => conversationData.bookMarked.get(d.data.uuid)) .attr("visibility", "visible"); nodesEnter.on("contextmenu", nodesInAndOutKit.onContextMenu) nodesEnter.on("mouseover", nodesInAndOutKit.handleMouseOver); nodesEnter.on("mouseout", nodesInAndOutKit.handleMouseOut); nodesEnter.on('click', nodesInAndOutKit.showNode); nodes.merge(nodesEnter) .attr("transform", d => `translate(${d.x},${d.y})`); nodes.exit().remove(); drawThumbnailSVG(); updateStylesBasedOnTheme(); } //nodesInAndOutKit const nodesInAndOutKit = { highlightDescendantsOnDrag: function (node) { gNodes.selectAll(".node") .classed("descendant-dragging", d => nodesInAndOutKit.isDescendant(node, d)); }, isDescendant: function (parent, node) { if (node === parent) return false; while (node) { if (node === parent) { return true; } node = node.parent; } return false; }, highlightDescendantLinks: function (node) { gLinks.selectAll(".link") .classed("descendant-highlighted", function (d) { return nodesInAndOutKit.isDescendant(node, d.target); }); }, showNode: function (d) { const existingComment = d.data.comment; if (existingComment && existingComment !== '') { commentTextarea.value = existingComment; } else { commentTextarea.value = ''; } const nodeElement = d3.select(this); const isAlreadySelected = nodeElement.classed("selectedNode"); if (isAlreadySelected) { contentDiv.style.display = 'none'; nodeElement.classed("selectedNode", false); if (commentForm.style.display === 'block') { commentForm.style.display = 'none'; } } else { if (d.data.type === "用户") { talkingPerson.innerHTML = conversationData.participants.user.avatarHTML; } else if (d.data.type === "chatGPT") { talkingPerson.innerHTML = (conversationData.participants.gpt.type === "GPT-3") ? GPT_Avatar_Config.gpt3_Inner_Html : GPT_Avatar_Config.gpt4_Inner_Html; } else { talkingPerson.innerHTML = ''; } gNodes.selectAll(".node") .classed("selectedNode", false); nodeElement.classed("selectedNode", true); contentDiv.style.display = 'block'; log("nodeElement:", nodeElement); contentDiv.dataset.curDisplay = d.data.uuid; let selectedNodeContent = document.getElementById("nodeContent"); const htmlClass = document.documentElement.getAttribute('class'); contentDiv.style.backgroundColor = htmlClass === "dark" ? d.data.type === "chatGPT" ? 'rgb(68,70,84)' : 'rgb(51,53,65)' : d.data.type === "chatGPT" ? 'rgb(247,247,248)' : 'rgb(256,256,256)'; contentDiv.style.boxShadow = htmlClass === "dark" ? '0px 4px 20px rgba(255, 255, 255, 0.1)' : '0px 4px 20px rgba(0, 0, 0, 0.1)'; nodesInAndOutKit.updateSelectedNodeContentDiv(d, selectedNodeContent); selectedNodeContent.removeEventListener('click', nodesInAndOutKit.handleClick); selectedNodeContent.addEventListener('click', nodesInAndOutKit.handleClick); var initialHeight = contentDiv.offsetHeight; selectedNodeContent.style.height = initialHeight - 60 + 'px'; //selectedNodeContent.style.maxHeight = '600px'; } }, createTalkingPersonElement: function (d) { const talkingPerson1 = document.createElement('div'); talkingPerson1.style.float = 'left'; talkingPerson1.style.marginRight = '8px'; talkingPerson1.style.background = 'none'; talkingPerson1.style.height = 'auto'; if (d.data.type === "用户") { talkingPerson1.innerHTML = conversationData.participants.user.avatarHTML; const img = talkingPerson1.querySelector('img'); if (img) { img.style.display = 'block'; img.style.margin = '0'; img.style.height = '35px'; img.style.width = '35px'; } } else if (d.data.type === "chatGPT") { talkingPerson1.innerHTML = (conversationData.participants.gpt.type === "GPT-3") ? GPT_Avatar_Config.gpt3_Inner_Html : GPT_Avatar_Config.gpt4_Inner_Html; } else { talkingPerson1.innerHTML = ''; } return talkingPerson1; }, updateSelectedNodeContentDiv: function (d, targetDiv) { const talkingPerson1 = nodesInAndOutKit.createTalkingPersonElement(d); // let commentHTML = d.data.comment ? `注释: ${d.data.comment}
` : ''; // if (d.data.type === "chatGPT") { // targetDiv.innerHTML = commentHTML; // targetDiv.appendChild(talkingPerson1); // targetDiv.innerHTML += `${d.data.content}`; // } else { // targetDiv.innerHTML = commentHTML; // targetDiv.appendChild(talkingPerson1); // let contentSpan = document.createElement('span'); // contentSpan.className = 'content-text'; // contentSpan.textContent = d.data.content; // targetDiv.appendChild(contentSpan); // } let commentHTML = d.data.comment ? `注释: ${d.data.comment}
` : ''; targetDiv.innerHTML = commentHTML; targetDiv.appendChild(talkingPerson1); let contentSpan = document.createElement('span'); contentSpan.className = 'content-text'; if (d.data.type === "chatGPT") { contentSpan.innerHTML = d.data.content; // 如果需要保留HTML标签 } else { contentSpan.textContent = d.data.content; // 如果只需要纯文本 } targetDiv.appendChild(contentSpan); }, handleClick: function (e) { if (e.target.closest('button.flex.ml-auto.gizmo\\:ml-0.gap-2.items-center')) { var codeBlock = e.target.closest('.bg-black').querySelector('code').innerText; nodesInAndOutKit.copyToClipboard(codeBlock); } }, copyToClipboard: function (text) { var textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); document.execCommand('Copy'); textArea.remove(); ButtonOperations.showUserNotification(translate("codeCopiedToClipboard")); }, handleMouseOver: function (d) { curMouseOnUUID = d.data.uuid + "-window"; d3.select(this).style("cursor", "pointer"); d3.select(this).select("circle") .style("stroke", "#FFFFFF") .style("stroke-width", "3px") .style("cursor", "pointer"); if (d.parent) { d3.selectAll(".node") .filter(function (data) { return data === d.parent; }) .select("circle") .style("stroke", "cyan") .style("stroke-width", "3px"); } const ancestors = d.ancestors(); gLinks.selectAll(".link") .filter(link => ancestors.indexOf(link.target) > -1) .classed("highlighted", true); nodesInAndOutKit.highlightDescendantLinks(d); let windowId = d.data.uuid + "-window"; if (!document.getElementById(windowId)) { temporaryWindowKit.createContentWindowForNode(d); temporaryWindowKit.positionContentWindow(d, event.pageX, event.pageY); } }, handleMouseOut: function (d) { curMouseOnUUID = null; setTimeout(() => { if (curMouseOnUUID !== d.data.uuid + "-window") { let windowElem = document.getElementById(d.data.uuid + "-window"); if (windowElem && windowElem.id === d.data.uuid + "-window") { windowElem.remove(); } } }, 300); gLinks.selectAll(".link.highlighted") .classed("highlighted", false); gLinks.selectAll(".link.descendant-highlighted") .classed("descendant-highlighted", false); d3.select(this).select("circle") .style("stroke", null) .style("stroke-width", null); if (d.parent) { d3.selectAll(".node") .filter(function (data) { return data === d.parent; }) .select("circle") .style("stroke", null) .style("stroke-width", null); } }, onContextMenu: function (d) { log("右击了节点!"); d3.event.stopPropagation() d3.event.preventDefault(); const tag = d3.select(this).select(".emoji-tag"); if (tag.attr("visibility") === "hidden") { tag.attr("visibility", "visible"); conversationData.bookMarked.set(d.data.uuid, true); dbOperations.saveConversationsData(conversationData) .then(() => { log("已保存变化!",); }) .catch(error => { console.error("Error saving data:", error); }); } else { tag.attr("visibility", "hidden"); conversationData.bookMarked.delete(d.data.uuid); dbOperations.saveConversationsData(conversationData) .then(() => { }) .catch(error => { console.error("Error saving data:", error); }); } }, } //temporaryWindowKit const temporaryWindowKit = { positionContentWindow: function (d, x, y) { let windowElem = document.getElementById(d.data.uuid + "-window"); if (windowElem) { windowElem.style.top = (y + 15) + 'px'; windowElem.style.left = (x + 20) + 'px'; } }, createContentWindowForNode: function (d) { let elements = document.querySelectorAll('.temporalityContentLayoutDiv'); elements.forEach(element => { element.remove(); }); let windowElem = document.createElement('div'); windowElem.className = "temporalityContentLayoutDiv"; windowElem.id = d.data.uuid + "-window"; windowElem.style.position = 'absolute'; windowElem.style.width = '600px'; windowElem.style.maxHeight = '500px'; windowElem.style.padding = '20px'; windowElem.style.fontFamily = 'Arial, sans-serif'; windowElem.style.fontSize = '16px'; windowElem.style.lineHeight = '1.6'; windowElem.style.border = '1px solid #ccc'; windowElem.style.borderRadius = '5px'; windowElem.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)'; windowElem.style.overflow = 'auto'; windowElem.style.opacity = '0.8'; nodesInAndOutKit.updateSelectedNodeContentDiv(d, windowElem); const htmlClass = document.documentElement.getAttribute('class'); windowElem.classList.add('markdown', 'prose', 'w-full', 'break-words', 'dark:prose-invert', htmlClass); windowElem.style.backgroundColor = htmlClass === "dark" ? d.data.type === "chatGPT" ? 'rgb(68,70,84)' : 'rgb(51,53,65)' : d.data.type === "chatGPT" ? 'rgb(247,247,248)' : 'rgb(256,256,256)'; windowElem.style.boxShadow = htmlClass === "dark" ? '0px 4px 20px rgba(255, 255, 255, 0.1)' : '0px 4px 20px rgba(0, 0, 0, 0.1)'; windowElem.addEventListener('click', nodesInAndOutKit.handleClick); document.body.appendChild(windowElem); windowElem.addEventListener('mouseenter', temporaryWindowKit.windowEnter); windowElem.addEventListener('mouseleave', temporaryWindowKit.windowLeave); }, windowEnter: function (e) { curMouseOnUUID = e.target.id; }, windowLeave: function (e) { curMouseOnUUID = null; setTimeout(() => { if (curMouseOnUUID !== e.target.id) { e.target.remove(); } }, 300); }, } //---ContentKit---// const contentDiv = document.createElement('div'); const selectedNodeContent = document.createElement('div'); const contentHeader = document.createElement('div'); const copyButton = document.createElement('button'); const goToNodeButton = document.createElement('button'); const talkingPerson = document.createElement('div'); const closeButton = document.createElement('button'); const commentButton = document.createElement('button'); const commentForm = document.createElement('div'); const commentLabel = document.createElement('label'); const commentTextarea = document.createElement('textarea'); const submitButton = document.createElement('button'); const cancelButton = document.createElement('button'); const clearButton = document.createElement('button'); function updateStylesBasedOnTheme() { const htmlClass = document.documentElement.getAttribute('class'); contentDiv.style.backgroundColor = htmlClass === "dark" ? 'rgb(68,70,84)' : 'rgb(256,256,256)'; contentDiv.style.boxShadow = htmlClass === "dark" ? '0px 4px 20px rgba(255, 255, 255, 0.1)' : '0px 4px 20px rgba(0, 0, 0, 0.1)'; const links = gLinks.selectAll(".link"); if (htmlClass === "dark") { links .attr("stroke", "white") } else { links .attr("stroke", "black") } commentTextarea.style.background = htmlClass === "dark" ? 'rgb(68,70,84)' : 'rgb(256,256,256)'; } updateStylesBasedOnTheme(); const HTMLClassObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.attributeName === 'class') { updateStylesBasedOnTheme(); } } }); HTMLClassObserver.observe(document.documentElement, {attributes: true}); let isRightMouseDown = false; let isLeftMouseDown = false; let initialMouseX, initialMouseY, initialDivX, initialDivY; const ContentKit = { init: function () { this.createContentDivs(); this.addEventListeners(); }, createContentDivs: function () { selectedNodeContent.id = 'nodeContent'; copyButton.id = 'copyButton'; goToNodeButton.id = 'goToNodeButton'; contentHeader.id = 'contentHeader'; commentButton.id = 'commentButton'; contentHeader.style.fontFamily = 'Times New Roman'; contentHeader.textContent = translate("nodeDetails"); contentHeader.style.fontSize = '20px'; contentHeader.style.fontWeight = 'bold'; contentHeader.style.marginBottom = '10px'; contentHeader.style.display = 'flex'; contentHeader.style.justifyContent = 'space-between'; contentHeader.style.alignItems = 'center'; const htmlClass = document.documentElement.getAttribute('class'); contentDiv.id = 'contentDiv'; contentDiv.style.position = 'fixed'; contentDiv.style.top = '10px'; contentDiv.style.right = '10px'; contentDiv.style.width = '400px'; contentDiv.style.padding = '10px'; contentDiv.style.borderRadius = '8px'; contentDiv.style.border = '8px solid #ddd'; contentDiv.style.display = 'none'; contentDiv.style.fontFamily = 'Arial, sans-serif'; contentDiv.style.fontSize = '14px'; contentDiv.style.lineHeight = '1.6'; contentDiv.style.overflow = 'hidden'; contentDiv.style.userSelect = 'text'; let windowHeight = window.innerHeight; contentDiv.style.height = windowHeight - 20 + 'px'; selectedNodeContent.style.paddingTop = '10px'; selectedNodeContent.style.paddingRight = '10px'; selectedNodeContent.style.paddingBottom = '10px'; selectedNodeContent.style.paddingLeft = '10px'; selectedNodeContent.style.userSelect = 'text'; selectedNodeContent.style.overflow = 'auto'; //selectedNodeContent.classList.add('markdown', 'prose', 'w-full', 'break-words', 'dark:prose-invert', htmlClass); selectedNodeContent.classList.add('markdown', 'prose', 'w-full', 'break-words', 'dark:prose-invert'); copyButton.innerHTML = `📋`; copyButton.style.border = 'none'; copyButton.style.cursor = 'pointer'; copyButton.style.fontSize = '20px'; copyButton.style.position = 'absolute'; copyButton.style.top = '6px'; copyButton.style.right = '150px'; copyButton.style.background = 'none'; copyButton.style.border = 'none'; copyButton.style.cursor = 'pointer'; goToNodeButton.textContent = '🚩'; goToNodeButton.style.position = 'absolute'; goToNodeButton.style.top = '7px'; goToNodeButton.style.right = '90px'; goToNodeButton.style.background = 'none'; goToNodeButton.style.border = 'none'; goToNodeButton.style.fontSize = '18px'; goToNodeButton.style.cursor = 'pointer'; commentButton.textContent = "🖊"; commentButton.style.position = 'absolute'; commentButton.style.top = '8px'; commentButton.style.right = '210px'; commentButton.style.background = 'none'; commentButton.style.border = 'none'; commentButton.style.fontSize = '18px'; commentButton.style.cursor = 'pointer'; talkingPerson.id = 'talkingPerson'; talkingPerson.style.position = 'absolute'; talkingPerson.style.top = '8px'; talkingPerson.style.right = '240px'; talkingPerson.style.background = 'none'; const closeIconSVG = ''; closeButton.innerHTML = closeIconSVG; closeButton.style.position = 'absolute'; closeButton.style.top = '10px'; closeButton.style.right = '40px'; closeButton.style.backgroundColor = 'white'; closeButton.style.border = 'none'; closeButton.style.cursor = 'pointer'; closeButton.style.fontSize = '30px'; commentForm.id = 'commentForm'; commentForm.style.position = 'fixed'; ContentKit.positionCommentFormRelativeToContentDiv(); commentForm.style.display = 'none'; commentLabel.setAttribute('for', 'comment'); commentLabel.innerText = translate("enterComment") + ":"; commentForm.appendChild(commentLabel); commentTextarea.id = 'commentText'; commentTextarea.rows = '5'; commentForm.appendChild(commentTextarea); submitButton.id = 'submitComment'; submitButton.innerText = translate("userCommentSave"); cancelButton.className = 'commentHoverEffect'; clearButton.className = 'commentHoverEffect'; submitButton.className = 'commentHoverEffect'; commentForm.appendChild(submitButton); commentForm.appendChild(cancelButton); commentForm.appendChild(clearButton); document.body.appendChild(commentForm); contentHeader.appendChild(copyButton); contentHeader.appendChild(goToNodeButton); contentHeader.appendChild(closeButton); contentHeader.appendChild(commentButton); contentHeader.appendChild(talkingPerson); contentDiv.appendChild(selectedNodeContent); contentDiv.insertBefore(contentHeader, selectedNodeContent); document.body.appendChild(contentDiv); }, positionCommentFormRelativeToContentDiv: function () { const rect = contentDiv.getBoundingClientRect(); commentForm.style.position = 'fixed'; commentForm.style.left = (rect.left - 300) + 'px'; commentForm.style.top = rect.top + 'px'; const commentRect = commentForm.getBoundingClientRect(); }, addEventListeners: function () { talkingPerson.addEventListener('mouseenter', function (e) { talkingPerson.style.cursor = 'grab'; }); talkingPerson.addEventListener('mousedown', function (e) { if (e.button === 0) { e.preventDefault(); e.stopPropagation(); isLeftMouseDown = true; initialMouseX = e.clientX; initialMouseY = e.clientY; initialDivX = contentDiv.offsetLeft; initialDivY = contentDiv.offsetTop; let rect = contentDiv.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.body.style.cursor = 'grabbing'; document.addEventListener('mousemove', moveContentDiv); } }); contentDiv.addEventListener('mousedown', function (e) { if (e.button === 2) { e.preventDefault(); isRightMouseDown = true; initialMouseX = e.clientX; initialMouseY = e.clientY; initialDivX = contentDiv.offsetLeft; initialDivY = contentDiv.offsetTop; let rect = contentDiv.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.body.style.cursor = 'move'; document.addEventListener('mousemove', moveContentDiv); } }); document.addEventListener('mouseup', function (e) { if (isRightMouseDown && e.button === 2 || isLeftMouseDown && e.button === 0) { e.preventDefault(); isRightMouseDown = false; document.body.style.cursor = ''; talkingPerson.style.cursor = 'grab'; document.removeEventListener('mousemove', moveContentDiv); } }); contentDiv.addEventListener('contextmenu', function (e) { e.preventDefault(); }); // onMouseDown: function (e) { // if (e.button !== 0) return; // let rect = treeMainBtn.getBoundingClientRect(); // offsetX = e.clientX - rect.left; // offsetY = e.clientY - rect.top; // isDragging = true; // window.addEventListener("mousemove", ButtonOperations.onMouseMove); // window.addEventListener("mouseup", ButtonOperations.onMouseUp); // }, function moveContentDiv(e) { if (isRightMouseDown || isLeftMouseDown) { ButtonOperations.hideMenu(); let top = e.clientY - offsetY; let left = e.clientX - offsetX; let maxWidth = window.innerWidth; let maxHeight = window.innerHeight; let elementWidth = contentDiv.offsetWidth; let elementHeight = contentDiv.offsetHeight; if (left < 0) { left = 0; } else if (left > maxWidth - elementWidth) { left = maxWidth - elementWidth; } if (top < 0) { top = 0; } else if (top > maxHeight - elementHeight) { top = maxHeight - elementHeight; } contentDiv.style.left = left + "px"; contentDiv.style.top = top + "px"; contentDiv.style.right = "auto"; contentDiv.style.bottom = "auto"; document.body.style.cursor = isRightMouseDown ? 'move' : 'grabbing'; talkingPerson.style.cursor = 'grabbing'; ContentKit.positionCommentFormRelativeToContentDiv(); } } // function moveContentDiv(e) { // if (isRightMouseDown || isLeftMouseDown) { // const dx = e.clientX - initialMouseX; // const dy = e.clientY - initialMouseY; // // let newLeft = initialDivX + dx; // let newTop = initialDivY + dy; // // let maxWidth = window.innerWidth; // let maxHeight = window.innerHeight; // let elementWidth = contentDiv.offsetWidth; // let elementHeight = contentDiv.offsetHeight; // // // 检查右边界 // if (newLeft > maxWidth - elementWidth) { // newLeft = maxWidth - elementWidth; // } // // // 检查上边界 // if (newTop < 0) { // newTop = 0; // } // // contentDiv.style.left = newLeft + 'px'; // contentDiv.style.top = newTop + 'px'; // // document.body.style.cursor = isRightMouseDown ? 'move' : 'grabbing'; // talkingPerson.style.cursor = 'grabbing'; // // ContentKit.positionCommentFormRelativeToContentDiv(); // } // } closeButton.addEventListener('click', function () { contentDiv.style.display = 'none'; if (commentForm.style.display === 'block') { commentForm.style.display = 'none'; } }); copyButton.addEventListener("click", function () { ContentKit.copyToClipboard(selectedNodeContent.innerHTML.replace(/<[^>]*>/g, ' ')); ButtonOperations.showUserNotification(translate("contentCopied")); }); commentButton.addEventListener('click', this.commentToSave); goToNodeButton.addEventListener('click', () => treeOperation.jumpToDialogueItem(contentDiv.dataset.curDisplay)); contentDiv.addEventListener('mousemove', ContentKit.handleContentDivMouseMove); contentDiv.addEventListener('mousedown', ContentKit.handleContentDivMouseDown); contentDiv.onmouseleave = function () { contentDiv.style.cursor = 'null'; }; }, handleContentDivMouseMove: function (e) { var borderWidth = 8; var oBoxW = contentDiv.offsetWidth - borderWidth; var oBoxH = contentDiv.offsetHeight - borderWidth; var x = e.clientX - contentDiv.getBoundingClientRect().left; var y = e.clientY - contentDiv.getBoundingClientRect().top; contentDiv.style.cursor = ''; if (x < borderWidth) { contentDiv.style.cursor = 'w-resize'; } else if (x > oBoxW) { contentDiv.style.cursor = 'e-resize'; } if (y < borderWidth) { contentDiv.style.cursor = 'n-resize'; } else if (y > oBoxH) { contentDiv.style.cursor = 's-resize'; } if (x < borderWidth && y < borderWidth) { contentDiv.style.cursor = 'nw-resize'; } else if (x > oBoxW && y < borderWidth) { contentDiv.style.cursor = 'ne-resize'; } else if (x < borderWidth && y > oBoxH) { contentDiv.style.cursor = 'sw-resize'; } else if (x > oBoxW && y > oBoxH) { contentDiv.style.cursor = 'se-resize'; } }, handleContentDivMouseDown: function (e) { var borderWidth = 8; e = e || event; var d = null; var oBoxL = contentDiv.offsetLeft; var oBoxT = contentDiv.offsetTop; var oBoxW = contentDiv.offsetWidth - borderWidth; var oBoxH = contentDiv.offsetHeight - borderWidth; var initialWidth = contentDiv.offsetWidth; var initialHeight = contentDiv.offsetHeight; var x = e.clientX - oBoxL; var y = e.clientY - oBoxT; var directions = { top: y < borderWidth, bottom: y > oBoxH, left: x < borderWidth, right: x > oBoxW }; var corners = [ {corner: 'LT', conditions: ['left', 'top']}, {corner: 'LB', conditions: ['left', 'bottom']}, {corner: 'RT', conditions: ['right', 'top']}, {corner: 'RB', conditions: ['right', 'bottom']} ]; var d = Object.keys(directions).find(key => directions[key]) || null; corners.forEach(corner => { if (corner.conditions.every(condition => directions[condition])) { d = corner.corner; } }); if (!d) { return; } var startX = e.clientX; var startY = e.clientY; function handleMouseMove(e) { e = e || event; var dx = e.clientX - startX; var dy = e.clientY - startY; const cursors = { 'left': 'ew-resize', 'right': 'ew-resize', 'top': 'ns-resize', 'bottom': 'ns-resize', 'LB': 'nesw-resize', 'LT': 'nwse-resize', 'RB': 'nwse-resize', 'RT': 'nesw-resize' }; const MIN_WIDTH = 400; const MIN_HEIGHT = 200; function updateStyle(d, dx, dy) { let newWidth, newHeight; switch (d) { case 'left': case 'LB': case 'LT': newWidth = initialWidth - dx; if (d !== 'left') { newHeight = d === 'LB' ? initialHeight + dy : initialHeight - dy; } break; case 'right': case 'RB': case 'RT': newWidth = initialWidth + dx; if (d !== 'right') { newHeight = d === 'RB' ? initialHeight + dy : initialHeight - dy; } break; case 'top': case 'bottom': newHeight = d === 'top' ? initialHeight - dy : initialHeight + dy; break; } if (newWidth >= MIN_WIDTH) { contentDiv.style.width = newWidth + 'px'; if (['left', 'LB', 'LT'].includes(d)) { contentDiv.style.left = (oBoxL + dx) + 'px'; } } if (newHeight >= MIN_HEIGHT) { contentDiv.style.height = newHeight + 'px'; selectedNodeContent.style.height = (newHeight - 60) + 'px'; if (['top', 'LT', 'RT'].includes(d)) { contentDiv.style.top = (oBoxT + dy) + 'px'; } } document.body.style.cursor = cursors[d]; } switch (d) { case 'left': case 'right': case 'top': case 'bottom': case 'LB': case 'LT': case 'RB': case 'RT': updateStyle(d, dx, dy); break; } if (commentForm.style.display === 'block') { ContentKit.positionCommentFormRelativeToContentDiv(); } return false; } function handleMouseUp() { log("mouseup!"); document.body.style.cursor = ''; contentDiv.style.cursor = ''; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); if (e.preventDefault) { e.preventDefault(); } }, copyToClipboard: function (text) { const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); }, commentToSave: function (d) { let curUUID = contentDiv.dataset.curDisplay; log("curUUID:", curUUID); if (commentForm.style.display !== 'block') { commentForm.style.display = 'block'; } else { commentForm.style.display = 'none'; } ContentKit.positionCommentFormRelativeToContentDiv(); let selectedNode = gNodes.selectAll(".node") .filter(function (d) { return d.data.uuid === curUUID; }); log("commentForm Node:", selectedNode); const existingComment = selectedNode.data.comment; if (existingComment && existingComment !== '') { commentTextarea.value = existingComment; } if (!cancelButton.hasListener) { cancelButton.innerText = translate("userCommentCancel"); cancelButton.addEventListener('click', function () { commentForm.style.display = 'none'; }); cancelButton.hasListener = true; } if (!clearButton.hasListener) { clearButton.innerText = translate("userCommentClear"); clearButton.addEventListener('click', function () { commentTextarea.value = ''; }); clearButton.hasListener = true; } if (!submitButton.hasListener) { log("curUUID:", curUUID); commentForm.style.display = 'block'; submitButton.addEventListener('click', function () { let commentValue = commentTextarea.value; let curUUID = contentDiv.dataset.curDisplay; let selectedNode = gNodes.selectAll(".node") .filter(function (d) { return d.data.uuid === curUUID; }); let selectedNodeData; selectedNodeData = selectedNode.data()[0]; let selectedMapNode; selectedMapNode = conversationData.uuid2nodeMap.get(curUUID); if (commentValue.trim() === '') { if (confirm(translate("emptyCommentPrompt"))) { ButtonOperations.showUserNotification(translate("commentSetToEmpty")); selectedNodeData.data.comment = ''; log("renew selectedNodeData:", selectedNodeData); log("selectedNodeData.comment:", selectedNodeData.data.comment); selectedMapNode.comment = ''; const existingComment = selectedNode.data.comment; if (existingComment && existingComment !== '') { commentTextarea.value = existingComment; } dbOperations.saveConversationsData(conversationData) .then(() => { }) .catch(error => { console.error("Error saving data:", error); }); log("newData", conversationData); commentForm.style.display = 'none'; selectedNodeData.data.comment = ""; nodesInAndOutKit.updateSelectedNodeContentDiv(selectedNodeData, selectedNodeContent); } else { ButtonOperations.showUserNotification(translate("enterCommentFirst"), "alert"); } } else { ButtonOperations.showUserNotification(translate("commentSaved")); try { selectedNodeData.data.comment = commentValue; log("renew selectedNodeData:", selectedNodeData); log("selectedNodeData.comment:", selectedNodeData.data.comment); selectedMapNode.comment = commentValue; const existingComment = selectedNode.data.comment; if (existingComment && existingComment !== '') { commentTextarea.value = existingComment; } dbOperations.saveConversationsData(conversationData) .then(() => { nodesInAndOutKit.updateSelectedNodeContentDiv(selectedNodeData, selectedNodeContent); }) .catch(error => { console.error("Error saving data:", error); }); commentForm.style.display = 'none'; } catch (error) { //log("error:", error); } } }); submitButton.hasListener = true; } }, } ContentKit.init(); //___ContentKit___// //---settingsKit---// let settingsDiv = document.createElement('div'); let zoomInButton = document.createElement('div'); let zoomOutButton = document.createElement('div'); let refreshTreeButton = document.createElement('div'); let thumbNailButton = document.createElement('div'); let undoButton = document.createElement('div'); let redoButton = document.createElement('div'); let deleteDiv = document.createElement('div'); let feedbackDiv = document.createElement('div'); let settingsContainer = document.createElement('div'); let rightMiddleContainer = document.createElement('div'); let colorSelectDiv = document.createElement('div'); const tooltipDiv = document.createElement('div'); let rightMiddleMenu = document.createElement("div"); let scaleIncrementSmall = 0.1; let scaleIncrementLarge = 0.3; let centerX = window.innerWidth / 2; let centerY = window.innerHeight / 3; let undoStack = []; let redoStack = []; let newOperation = {}; const settingsKit = { init: function () { this.createSettingDivs(); this.addEventListeners(); }, createSettingDivs: function () { settingsDiv.id = "settingsDiv"; settingsDiv.style.fontSize = '24px'; settingsContainer.appendChild(settingsDiv); settingsContainer.style.display = 'none'; rightMiddleContainer.style.display = 'none'; const settingSVG = ''; settingsDiv.innerHTML = settingSVG; zoomInButton.className = "actionDiv"; zoomInButton.id = 'plusDiv'; zoomOutButton.className = "actionDiv"; zoomOutButton.id = 'minusDiv'; zoomInButton.innerText = '➕'; zoomOutButton.innerText = '➖'; settingsContainer.appendChild(zoomInButton); settingsContainer.appendChild(zoomOutButton); thumbNailButton.id = 'thumbNailDiv'; thumbNailButton.className = "actionDiv"; thumbNailButton.innerText = '🌐'; settingsContainer.appendChild(thumbNailButton); refreshTreeButton.id = 'refreshTree'; refreshTreeButton.className = "actionDiv"; refreshTreeButton.innerText = '🔄️'; settingsContainer.appendChild(refreshTreeButton); undoButton.id = 'undoDiv'; undoButton.className = "actionDiv"; undoButton.innerText = '⏪'; settingsContainer.appendChild(undoButton); redoButton.id = 'redoDiv'; redoButton.className = "actionDiv"; redoButton.innerText = '⏩'; settingsContainer.appendChild(redoButton); deleteDiv.id = 'deleteDiv'; deleteDiv.className = "actionDiv"; deleteDiv.innerText = '🗑️'; settingsContainer.appendChild(deleteDiv); feedbackDiv.id = 'feedbackDiv'; feedbackDiv.className = "rightMiddleDiv"; feedbackDiv.innerText = '✉️'; rightMiddleContainer.appendChild(feedbackDiv); rightMiddleMenu.style.display = 'none'; rightMiddleMenu.id = 'rightMiddleMenu'; rightMiddleContainer.appendChild(rightMiddleMenu); colorSelectDiv.id = "colorSelectDiv"; colorSelectDiv.className = "rightMiddleDiv"; colorSelectDiv.innerText = '🎨'; rightMiddleContainer.appendChild(colorSelectDiv); //rightMiddleDiv.style.display = 'block'; if (globalUserLang.startsWith('zh')) { tooltipDiv.innerHTML = `
点击参与问卷调查 (腾讯问卷)
问卷二维码
`; } else { tooltipDiv.innerHTML = `

Click to take the survey
(Google Forms)

Survey Code
`; } tooltipDiv.style.display = 'none'; rightMiddleContainer.appendChild(tooltipDiv); document.body.appendChild(settingsContainer); document.body.appendChild(rightMiddleContainer); }, toggleColorSelectShow: function () { if (rightMiddleMenu.style.display === 'flex') { rightMiddleMenu.style.display = 'none'; return; } rightMiddleMenu.style.display = 'flex'; document.addEventListener('click', (e) => { if (!rightMiddleContainer.contains(e.target)) { rightMiddleMenu.style.display = 'none'; } }); function rgbToHex(rgb) { const result = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/.exec(rgb); return result ? '#' + parseInt(result[1]).toString(16).padStart(2, '0') + parseInt(result[2]).toString(16).padStart(2, '0') + parseInt(result[3]).toString(16).padStart(2, '0') : null; } //let hexColor = rgbToHex(color); //let opacity = parseFloat(treeMainBtn.style.opacity); rightMiddleMenu.style.color = 'black'; rightMiddleMenu.innerHTML = `
`; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { let currentBackground = mainSvg.style.background; // 获取顶部和底部的颜色 let colorMatches = currentBackground.match(/rgba?\((\d{1,3}, \d{1,3}, \d{1,3})(?:, ([\d\.]+))?\)/g); let topColor = colorMatches && colorMatches[0]; let bottomColor = colorMatches && colorMatches[1]; // 获取 opacity let opacityMatch = topColor.match(/rgba?\(\d{1,3}, \d{1,3}, \d{1,3}, ([\d\.]+)\)/); let opacity = opacityMatch ? parseFloat(opacityMatch[1]) * 100 : 100; // 将 opacity 转换回百分比 log("opacity:", opacity); log("color:", rgbToHex(topColor)); document.getElementById('SVGOpacityPicker').value = opacity; document.getElementById('SVGColorTopPicker').value = rgbToHex(topColor); document.getElementById('SVGColorBottomPicker').value = rgbToHex(bottomColor); } let SVGOpacityPicker = document.getElementById('SVGOpacityPicker'); let SVGColorTopPicker = document.getElementById('SVGColorTopPicker'); let SVGColorBottomPicker = document.getElementById('SVGColorBottomPicker'); let chatGPTColorPicker = document.getElementById('chatGPTColorPicker'); let userColorPicker = document.getElementById('userColorPicker'); //改变 SVGOpacityPicker.addEventListener('input', settingsKit.colorAndOpacityKit.onSVGOpacityChange); SVGColorTopPicker.addEventListener('input', settingsKit.colorAndOpacityKit.onSVGColorTopChange); SVGColorBottomPicker.addEventListener('input', settingsKit.colorAndOpacityKit.onSVGColorBottomChange); // chatGPTColorPicker.addEventListener('input', settingsKit.colorAndOpacityKit.onChatGPTColorChange); // userColorPicker.addEventListener('input', settingsKit.colorAndOpacityKit.onUserColorChange); //改完 SVGOpacityPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onSVGOpacityChangeDone); SVGColorTopPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onSVGColorTopChangeDone); SVGColorBottomPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onSVGColorBottomChangeDone); // chatGPTColorPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onChatGPTColorChangeDone); // userColorPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onUserColorChangeDone); }, colorAndOpacityKit: { onSVGOpacityChange: function (e) { const opacity = e.target.value / 100; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { let currentBackground = mainSvg.style.background; // 使用正则表达式替换顶部和底部的opacity let newBackground = currentBackground.replace( /rgba\((\d{1,3}, \d{1,3}, \d{1,3}), [\d\.]+\)/g, `rgba($1, ${opacity})` ); mainSvg.style.background = newBackground; return newBackground; } return DEFAULT_MAINSVG_BACKGROUND; }, hexToRgb: function (hex) { var bigint = parseInt(hex.substring(1), 16); var r = (bigint >> 16) & 255; var g = (bigint >> 8) & 255; var b = bigint & 255; return r + "," + g + "," + b; }, onSVGColorTopChange: function (e) { const color = e.target.value; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { let currentBackground = mainSvg.style.background; // 使用正则表达式获取当前的opacity let opacityMatch = currentBackground.match(/rgba\(\d{1,3}, \d{1,3}, \d{1,3}, ([\d\.]+)\)/); let opacity = opacityMatch ? opacityMatch[1] : 1; // 默认值为1,如果没有匹配到opacity // 使用正则表达式替换顶部的颜色,同时保留原有的opacity let newBackground = currentBackground.replace( /linear-gradient\(to top, rgba\(\d{1,3}, \d{1,3}, \d{1,3}, [\d\.]+\) 0%/, `linear-gradient(to top, rgba(${settingsKit.colorAndOpacityKit.hexToRgb(color)}, ${opacity}) 0%` ); mainSvg.style.background = newBackground; return newBackground; } return DEFAULT_MAINSVG_BACKGROUND; }, onSVGColorBottomChange: function (e) { const color = e.target.value; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { let currentBackground = mainSvg.style.background; // 使用正则表达式获取当前的opacity let opacityMatch = currentBackground.match(/rgba\(\d{1,3}, \d{1,3}, \d{1,3}, ([\d\.]+)\) 100%/); let opacity = opacityMatch ? opacityMatch[1] : 1; // 默认值为1,如果没有匹配到opacity // 使用正则表达式替换底部的颜色,同时保留原有的opacity let newBackground = currentBackground.replace( /rgba\(\d{1,3}, \d{1,3}, \d{1,3}, [\d\.]+\) 100%/, `rgba(${settingsKit.colorAndOpacityKit.hexToRgb(color)}, ${opacity}) 100%` ); mainSvg.style.background = newBackground; return newBackground; } return DEFAULT_MAINSVG_BACKGROUND; }, onSVGOpacityChangeDone: function (e) { let newBackground = settingsKit.colorAndOpacityKit.onSVGOpacityChange(e); const newSettings = {id: 'mainSVG', background: newBackground}; dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onSVGColorTopChangeDone: function (e) { let newBackground = settingsKit.colorAndOpacityKit.onSVGColorTopChange(e); const newSettings = {id: 'mainSVG', background: newBackground}; dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onSVGColorBottomChangeDone: function (e) { let newBackground = settingsKit.colorAndOpacityKit.onSVGColorBottomChange(e); const newSettings = {id: 'mainSVG', background: newBackground}; dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, }, addEventListeners: function () { colorSelectDiv.addEventListener('click', settingsKit.toggleColorSelectShow); feedbackDiv.addEventListener('click', () => { // 检测用户的语言设置 if (globalUserLang.startsWith('zh')) { window.open('https://wj.qq.com/s2/13235492/27ec', '_blank'); } else { window.open('https://docs.google.com/forms/d/e/1FAIpQLSetbHqiS1GBM6bG0QaaKy9cN31jKXK76BcYCW8_wkRNH7I5kQ/viewform', '_blank'); } }); let feedbackTimeOut; feedbackDiv.addEventListener('mouseenter', function (e) { log("feedbackDiv mouseover"); clearTimeout(feedbackTimeOut); let elements = [feedbackDiv, tooltipDiv]; elements.forEach(element => { element.style.display = 'flex'; }); }); tooltipDiv.addEventListener('mouseenter', function (e) { log("tooltipDiv mouseover"); clearTimeout(feedbackTimeOut); tooltipDiv.style.display = 'block'; }); feedbackDiv.addEventListener('mouseleave', function () { log("feedbackDiv mouseleave"); feedbackTimeOut = setTimeout(() => { tooltipDiv.style.display = 'none'; }, 400); }); tooltipDiv.addEventListener('mouseleave', function (e) { log("tooltipDiv mouseleave"); feedbackTimeOut = setTimeout(() => { tooltipDiv.style.display = 'none'; }, 400); }); settingsDiv.addEventListener('click', function (e) { log("settingsContainer click"); let elements = document.querySelectorAll('.actionDiv'); if (deleteDiv.style.display !== 'flex') { elements.forEach(element => { element.style.display = 'flex'; }); } else { elements.forEach(element => { element.style.display = 'none'; }); } }); zoomInButton.addEventListener('click', this.zoomIn); zoomOutButton.addEventListener('click', this.zoomOut); thumbNailButton.addEventListener('click', this.toggleThumbnail); refreshTreeButton.addEventListener('click', this.refreshTree); undoButton.addEventListener('click', this.undo); redoButton.addEventListener('click', this.redo); deleteDiv.addEventListener('click', function () { settingsKit.handleTwoTypesOfDeleteConversationData(null, false); }); }, handleTwoTypesOfDeleteConversationData: async function (operatingLink, IsfromPanel) { if (!operatingLink) { if (states.treeUpdate.isDOMOperating || (!states.url.isForLiveValidURL && !states.url.isForDeletedValidURL) || conversationData.url === null) { return; } } let operatingURL; if (operatingLink === null && IsfromPanel === false) { operatingURL = conversationData.url; } else if (operatingLink && IsfromPanel === true) { operatingURL = operatingLink; } //log("going to confirm delete"); if (confirm(translate("confirmDeleteLinkData").replace('{item}', operatingURL))) { try { //await dbOperations.deleteConversationData(operatingURL); dbOperations.deleteConversationData(operatingURL) .then(() => dbOperations.initConversationData()) .then(information => { log("after_init_conversationData:", chatHistory); controlPanelKit.updateCategorySelect(); log("before_filteredConversations:", filteredConversations); filteredConversations = filteredConversations.filter(aconv => { return !(aconv.link === operatingURL) }); controlPanelKit.renderConversations(filteredConversations); log("after_filteredConversations:", filteredConversations); }) .catch(error => { console.error(error); }); log('Data deleted successfully'); ButtonOperations.showUserNotification(translate("conversationDataDeleted"), 'alert'); if (IsfromPanel) { return; } try { dbOperations.loadConversationsData(operatingURL).then(loadeddata => { log("Loaded data for URL:", loadeddata); conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); log("reNewedRoot:", root); const widthPerNode = 30; const heightPerNode = 30; treeLayout = d3.tree().nodeSize([widthPerNode, heightPerNode]); treeLayout(root); settingsKit.refreshTree(); drawMainSVG(); }).catch(error => { console.error("Error loading data:", error); }); } catch (loadError) { console.error('Error loading data:', loadError); } } catch (deleteError) { console.error('Error deleting data:', deleteError); } } else { log('数据未被删除!'); } }, zoomIn: function () { let currentScale = d3.zoomTransform(svg.node()).k; let increment = currentScale < 1 ? scaleIncrementSmall : scaleIncrementLarge; let newScale = currentScale + increment; if (newScale > 10) newScale = 10; svg.transition().duration(250) .call(zoom.scaleTo, newScale, [centerX, centerY]); }, zoomOut: function () { let currentScale = d3.zoomTransform(svg.node()).k; let decrement = currentScale <= 1 ? scaleIncrementSmall : scaleIncrementLarge; let newScale = currentScale - decrement; if (newScale < 0.1) newScale = 0.1; svg.transition().duration(250) .call(zoom.scaleTo, newScale, [centerX, centerY]); }, toggleThumbnail: function () { let currentVisibility = svgThumbnail.attr('visibility'); svgThumbnail.attr('visibility', currentVisibility === 'hidden' ? 'visible' : 'hidden'); log("currentVisibility:", currentVisibility); }, refreshTree: function () { undoStack = []; redoStack = []; svg.call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale("1")); treeLayout(root); const yOffset = window.innerHeight / 5 - root.x; const xOffset = window.innerWidth / 5 - root.x; root.each(d => { d.y += yOffset; }); root.each(d => { d.x += xOffset; }); drawMainSVG(); }, simulateMove: function (action, doType) { let nodeData; d3.selectAll("g.node") .filter(function (d) { return d.data.uuid === action.uuid; }) .each(function (d) { nodeData = d; }); if (!nodeData) { console.error("Node not found for UUID:", action.uuid); return; } let dx; let dy; if (doType === 'undo') { dx = action.startX - action.endX; dy = action.startY - action.endY; } else if (doType === 'redo') { dx = action.endX - action.startX; dy = action.endY - action.startY; } nodeData.x += dx; nodeData.y += dy; if (nodeData.children) { nodeData.children.forEach(child => { settingsKit.simulateMove({ uuid: child.data.uuid, startX: child.x, startY: child.y, endX: child.x - dx, endY: child.y - dy }, "undo"); }); } }, performAction: function (action) { undoStack.push(action); redoStack = []; }, redo: function () { if (redoStack.length <= 0) return; let action = redoStack.pop(); log("action:", action); if (action.type === 'drag') { settingsKit.simulateMove(action, "redo"); drawMainSVG(); } undoStack.push(action); }, undo: function () { if (undoStack.length <= 0) return; let action = undoStack.pop(); log("action:", action); if (action.type === 'drag') { settingsKit.simulateMove(action, "undo"); drawMainSVG(); } redoStack.push(action); }, }; settingsKit.init(); let searchContainer = document.createElement('div'); let searchIcon = document.createElement('div'); let searchBox = document.createElement('input'); let searchBtn = document.createElement('button'); let searchResultsCount = document.createElement('div'); let searchHistory = document.createElement('div'); let suggestionsContainer = document.createElement('div'); let matchedUUIDs = new Set(); let matchedArray = []; let currentTreeBreathingIndex = -1; const searchKit = { isSearchKitExpanded: false, stopBreathing: false, init: function () { settingsKit.refreshTree(); searchContainer.id = 'search-container'; searchContainer.style.display = 'none'; searchIcon.id = 'search-icon'; searchIcon.innerText = '🔍'; searchIcon.onclick = searchKit.toggleSearch; searchContainer.appendChild(searchIcon); searchBox.type = 'text'; searchBox.id = 'search-box'; suggestionsContainer.id = 'suggestions-container'; suggestionsContainer.className = 'suggestions-container'; searchContainer.appendChild(suggestionsContainer); searchBox.setAttribute("placeholder", translate("searchPlaceholder")); searchBox.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.keyCode === 13) { event.preventDefault(); event.stopPropagation(); searchKit.executeSearch(); } }); function updateSuggestions(suggestions) { suggestionsContainer.style.display = 'block'; suggestionsContainer.innerHTML = ''; suggestions.forEach(suggestion => { const item = document.createElement('div'); item.className = 'suggestion-item'; item.textContent = suggestion; const closeButton = document.createElement('span'); closeButton.textContent = 'x'; closeButton.className = 'close-button'; closeButton.onclick = (e) => { e.stopPropagation(); const index = suggestions.indexOf(suggestion); if (index > -1) { suggestions.splice(index, 1); dbOperations.addSearchRecord(searchHistoryRecord) .then(() => dbOperations.getSearchHistory()) .then(records => { log('Got search history:', records); }) .catch(error => console.error(error)); } updateSuggestions(suggestions); }; item.appendChild(closeButton); item.onclick = () => { searchBox.value = suggestion; clearSuggestions(); }; item.addEventListener('contextmenu', function (e) { e.preventDefault(); const index = suggestions.indexOf(suggestion); if (index > -1) { suggestions.splice(index, 1); } updateSuggestions(suggestions); dbOperations.addSearchRecord(searchHistoryRecord) .then(() => dbOperations.getSearchHistory()) .then(records => { log('Got search history:', records); }) .catch(error => console.error(error)); }); suggestionsContainer.appendChild(item); }); } function clearSuggestions() { suggestionsContainer.innerHTML = ''; suggestionsContainer.style.display = 'none'; } document.addEventListener('click', function (e) { if (!searchContainer.contains(e.target)) { clearSuggestions(); } }); searchBox.addEventListener('input', function () { const query = this.value.trim(); if (query.length > 0) { const suggestions = searchHistoryRecord.filter(existing => existing.includes(query)); updateSuggestions(suggestions); } else { clearSuggestions(); } }); searchContainer.appendChild(searchBox); searchBtn.id = 'search-btn'; searchBtn.innerText = translate("searchButton"); searchBtn.onclick = searchKit.executeSearch; searchBtn.style.border = 'none'; searchBtn.style.borderRadius = '3px'; searchBtn.style.height = '32px'; searchBtn.style.cursor = 'pointer'; searchBtn.style.position = 'relative'; searchBtn.style.top = '4px'; searchBtn.style.backgroundColor = 'rgb(241, 241, 241)'; searchContainer.appendChild(searchBtn); searchResultsCount.id = 'search-results-count'; searchResultsCount.style.borderRadius = '3px'; searchResultsCount.style.background = '#f1f1f1'; searchResultsCount.style.width = '80px'; searchResultsCount.style.height = '32px'; searchResultsCount.style.position = 'relative'; searchResultsCount.style.fontSize = '10px'; searchResultsCount.style.top = '4px'; searchResultsCount.style.left = '5px'; searchResultsCount.style.display = 'none'; const optionsButtons = document.createElement('div'); optionsButtons.style.width = '490px'; optionsButtons.style.position = 'absolute'; optionsButtons.style.top = '250px'; optionsButtons.style.left = '4px'; optionsButtons.style.display = 'flex'; optionsButtons.style.flexDirection = 'column'; optionsButtons.style.alignItems = 'center'; optionsButtons.style.background = 'none'; optionsButtons.id = "optionsButtons"; optionsButtons.style.display = 'none'; optionsButtons.style.marginTop = '10px'; const labelsDiv = document.createElement('div'); labelsDiv.style.display = 'flex'; labelsDiv.style.justifyContent = 'center'; labelsDiv.style.width = '100%'; labelsDiv.style.marginBottom = '20px'; const buttonsDiv = document.createElement('div'); buttonsDiv.style.display = 'flex'; buttonsDiv.style.justifyContent = 'center'; buttonsDiv.style.width = '100%'; const options = [translate("searchInContent"), translate("searchInComments"), translate("searchInBoth")]; options.forEach(option => { const label = document.createElement('label'); label.style.marginRight = '10px'; label.style.backgroundColor = 'rgb(221, 221, 221)'; label.style.padding = '5px'; label.style.borderRadius = '5px'; const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'searchOption'; radio.id = option.toLowerCase(); radio.value = option.toLowerCase(); if (option === translate("searchInContent")) radio.checked = true; label.appendChild(radio); label.appendChild(document.createTextNode(option)); labelsDiv.appendChild(label); }); optionsButtons.appendChild(labelsDiv); const navButtons = [translate("goToPrevious"), translate("goToNext")]; navButtons.forEach(btnText => { const btn = document.createElement('button'); btn.innerText = btnText; btn.style.marginRight = '10px'; btn.style.backgroundColor = 'rgb(221, 221, 221)'; btn.style.padding = '5px'; btn.style.borderRadius = '5px'; if (btnText === translate("goToNext")) { btn.addEventListener('click', function () { searchKit.goToNextMatch(); }); } if (btnText === translate("goToPrevious")) { btn.addEventListener('click', function () { searchKit.gpToPreviousMatch(); }); } buttonsDiv.appendChild(btn); }); optionsButtons.appendChild(buttonsDiv); searchContainer.appendChild(searchResultsCount); searchHistory.id = 'search-history'; searchContainer.appendChild(searchHistory); searchContainer.appendChild(optionsButtons); document.body.appendChild(searchContainer); }, toggleSearch: function () { const searchBox = document.getElementById('search-box'); const searchHistory = document.getElementById('search-history'); const searchIcon = document.getElementById('search-icon'); if (!this.isSearchKitExpanded) { searchContainer.style.display = 'flex'; searchContainer.style.width = '600px'; searchHistory.style.display = 'block'; searchBox.style.display = 'block'; anime({ targets: searchBox, width: '300px', duration: 500, easing: 'easeInOutQuart', opacity: 1, background: '#f1f1f1', complete: function () { searchBox.style.pointerEvents = 'auto'; searchBtn.style.display = 'block'; document.getElementById('search-history').style.display = 'flex'; searchResultsCount.style.display = 'block'; let opbtn = document.getElementById("optionsButtons"); opbtn.style.display = 'block'; searchKit.displaySearchHistory(); } }); anime({ targets: searchHistory, width: '500px', height: '300px', opacity: 1, duration: 500, easing: 'easeInOutQuart', background: '#f1f1f1', }); searchIcon.innerHTML = '❌'; this.isSearchKitExpanded = true; log("this.isSearchKitExpanded:", this.isSearchKitExpanded); } else { searchContainer.style.width = '50px'; suggestionsContainer.style.display = 'none'; gNodes.selectAll(".node") .select("circle") .interrupt() .attr("opacity", 1); gNodes.selectAll(".node") .each(function (d) { if (matchedUUIDs.has(d.data.uuid)) { d3.select(this) .select("text") .style("display", "none"); } }); let opbtn = document.getElementById("optionsButtons"); opbtn.style.display = 'none'; searchResultsCount.style.display = 'none'; anime({ targets: searchBox, width: '0px', duration: 500, easing: 'easeInOutQuart', opacity: 0, background: 'transparent', begin: function () { searchBtn.style.display = 'none'; }, complete: function () { searchHistory.style.display = 'none'; searchBox.style.display = 'none'; } }); anime({ targets: searchHistory, width: '20px', height: '0px', opacity: 0, duration: 500, easing: 'easeInOutQuart', background: '#f1f1f1', }); searchIcon.innerHTML = '🔍'; this.isSearchKitExpanded = false; log("this.isSearchKitExpanded:", this.isSearchKitExpanded); } }, escapeRegExp: function (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, executeSearch: function () { gNodes.selectAll(".node") .select("circle") .interrupt() .attr("opacity", 1); gNodes.selectAll(".node") .each(function (d) { if (matchedUUIDs.has(d.data.uuid)) { d3.select(this) .select("text") .style("display", "none"); } }); let rawQuery = document.getElementById('search-box').value; let trimmedQuery = rawQuery.trim(); if (trimmedQuery !== null && trimmedQuery !== '' && trimmedQuery !== ' ') { ButtonOperations.showUserNotification(translate("executeSearchWithQuery").replace(`{item}`, rawQuery)); searchKit.addSearchHistory(rawQuery); searchKit.displaySearchHistory(); const searchOptions = document.getElementsByName("searchOption"); let selectedOption = ""; let selectedNum = 0; for (const option of searchOptions) { if (option.checked) { selectedOption = option.value; break; } selectedNum++; } selectedOption = selectedNum === 0 ? translate("searchInContent") : selectedNum === 1 ? translate("searchInComments") : translate("searchInBoth"); let escapedQuery = searchKit.escapeRegExp(rawQuery); matchedArray = searchKit.bfsSearch(conversationData.rootNode, escapedQuery, selectedOption); matchedUUIDs = new Set(matchedArray.map(d => d.uuid)); log('results:', matchedArray); let matches = matchedArray.length; document.getElementById('search-results-count').innerText = translate("numberOfMatches").replace('{matches}', matches); dbOperations.addSearchRecord(searchHistoryRecord) .then(() => dbOperations.getSearchHistory()) .then(records => { log('Got search history:', records); }) .catch(error => console.error(error)); gNodes.selectAll(".node") .each(function (d) { if (matchedUUIDs.has(d.data.uuid)) { d3.select(this) .select("text") .style("display", "block"); } }); } else { alert("Please Enter Before Searching!"); } }, bfsSearch: function (tree, searchTerm, searchOption) { log("searchTerm:", searchTerm); log("in bfsSearch"); //log("searchOption:",searchOption, "translate:",translate("searchInContent"),"\n",translate("searchInComments"),"\n",translate("searchInBoth")); let queue = [tree]; let results = []; while (queue.length > 0) { let current = queue.shift(); if (searchOption === translate("searchInContent") && current.content) { if (current.content.match(new RegExp(searchTerm, 'i'))) results.push(current); } else if (searchOption === translate("searchInComments") && current.comment) { if (current.comment.match(new RegExp(searchTerm, 'i'))) results.push(current); } else if (searchOption === translate("searchInBoth") && (current.content || current.comment)) { if (current.content) { if (current.content.match(new RegExp(searchTerm, 'i'))) { results.push(current); } } if (current.comment) { if (current.comment.match(new RegExp(searchTerm, 'i'))) { results.push(current); } } } if (current.children) { queue.push(...current.children); } } return results; }, breathe: function (selection) { selection.select("circle") .transition() .duration(100) .attr("opacity", 0) .transition() .duration(100) .attr("opacity", 1) .on("end", function () { if (!this.stopBreathing) { searchKit.breathe(selection); } }); }, goToNextMatch: function () { log("In goToNextMatch"); if (matchedArray.length === 0) return; this.stopBreathing = true; gNodes.selectAll(".node") .select("circle") .interrupt() .attr("opacity", 1); log("matchedArray:", matchedArray); log("before_currentTreeBreathingIndex", currentTreeBreathingIndex); currentTreeBreathingIndex = (currentTreeBreathingIndex + 1) % matchedArray.length; log("after_currentTreeBreathingIndex", currentTreeBreathingIndex); log("matchedArray[currentTreeBreathingIndex]", matchedArray[currentTreeBreathingIndex]); let currentNodeUUID = matchedArray[currentTreeBreathingIndex].uuid; log("NextUUID:", currentNodeUUID); let nextNodeSelection = gNodes.selectAll(".node").filter(d => d.data.uuid === currentNodeUUID); log("nextNodeSelection:", nextNodeSelection); this.stopBreathing = false; searchKit.breathe(nextNodeSelection); searchKit.centerNodeOnScreen(nextNodeSelection); log("Out goToNextMatch"); }, gpToPreviousMatch: function () { if (matchedArray.length === 0) return; log("In goToNextMatch"); this.stopBreathing = true; gNodes.selectAll(".node") .select("circle") .interrupt() .attr("opacity", 1); log("matchedArray:", matchedArray); currentTreeBreathingIndex = (currentTreeBreathingIndex - 1 + matchedArray.length) % matchedArray.length; log("currentTreeBreathingIndex", currentTreeBreathingIndex); log("matchedArray[currentTreeBreathingIndex]", matchedArray[currentTreeBreathingIndex]); let currentNodeUUID = matchedArray[currentTreeBreathingIndex].uuid; log("NextUUID:", currentNodeUUID); let nextNodeSelection = gNodes.selectAll(".node").filter(d => d.data.uuid === currentNodeUUID); log("nextNodeSelection:", nextNodeSelection); this.stopBreathing = false; searchKit.breathe(nextNodeSelection); searchKit.centerNodeOnScreen(nextNodeSelection); log("Out goToNextMatch"); }, centerNodeOnScreen: function (node) { let nodeData = node.datum(); let nodeX = nodeData.x; let nodeY = nodeData.y; let newScale = 2; let horizontalTranslate = (window.innerWidth / 2) - (nodeX * newScale); let verticalTranslate = (window.innerHeight / 3) - (nodeY * newScale); let nodeTransform = d3.zoomIdentity.translate(horizontalTranslate, verticalTranslate).scale(newScale); svg.transition().duration(750).call(zoom.transform, nodeTransform); }, addSearchHistory: function (query) { if (searchHistoryRecord.includes(query)) { let index = searchHistoryRecord.indexOf(query); searchHistoryRecord.splice(index, 1); } searchHistoryRecord.unshift(query); }, displaySearchHistory: function () { let container = document.getElementById('search-history'); container.innerHTML = ''; let displayedCount = 0; searchHistoryRecord.forEach(item => { if (displayedCount >= 20) { return; } displayedCount++; let historyItem = document.createElement('div'); historyItem.className = 'history-item'; let text = document.createElement('div'); text.className = 'history-text'; text.innerText = item; historyItem.appendChild(text); historyItem.onclick = () => { searchBox.value = item; }; historyItem.style.cursor = 'pointer'; let deleteBtn = document.createElement('div'); deleteBtn.className = 'history-delete'; deleteBtn.innerText = 'x'; deleteBtn.onclick = function (e) { e.stopPropagation(); let index = searchHistoryRecord.indexOf(item); searchHistoryRecord.splice(index, 1); searchKit.displaySearchHistory(); dbOperations.addSearchRecord(searchHistoryRecord) .then(() => dbOperations.getSearchHistory()) .then(records => { log('Got search history:', records); }) .catch(error => console.error(error)); }; historyItem.appendChild(deleteBtn); container.appendChild(historyItem); }); }, }; searchKit.init(); let panelToggleButton = document.createElement('button'); // Create the 'managePanel' div let managePanel = document.createElement('div'); let topicSearchContainer = document.createElement('div'); let searchTopicBox = document.createElement('input'); let categoryEditor = document.createElement('div'); let categorySelect = document.createElement('select'); let conversationContainer = document.createElement('div'); let right_Top_togglePanel = document.createElement('div'); let filteredConversations = []; const controlPanelKit = { init: function () { panelToggleButton.id = "panelToggleButtonSVGShow"; panelToggleButton.innerText = translate("openAdminPanel"); panelToggleButton.style.display = 'none'; panelToggleButton.style.borderRadius = '12px'; // 更大的圆角 panelToggleButton.style.opacity = '0.9'; // 轻微调整透明度 panelToggleButton.style.background = 'linear-gradient(to right, #007BFF, #00C6FF)'; // 线性渐变背景 panelToggleButton.style.color = 'white'; // 文字颜色设置为白色 panelToggleButton.style.padding = '10px 20px'; // 添加一些内边距 panelToggleButton.style.fontWeight = 'bold'; // 粗体字 panelToggleButton.style.boxShadow = '0px 3px 5px rgba(0,0,0,0.2)'; // 添加微小的阴影 document.body.appendChild(panelToggleButton); managePanel.id = 'managePanel'; managePanel.style.display = 'none'; // Create the 'searchContainer' div topicSearchContainer.id = 'searchContainer'; topicSearchContainer.style.marginTop = '20px'; topicSearchContainer.style.marginLeft = '20px'; // Create the search input box searchTopicBox.type = 'text'; searchTopicBox.id = 'searchTopicBox'; searchTopicBox.setAttribute("placeholder", translate("searchPlaceholder")); // Append the search box to the 'searchContainer' div topicSearchContainer.appendChild(searchTopicBox); // Create the 'categoryEditor' div categoryEditor.id = 'categoryEditor'; categoryEditor.className = 'editor'; // Create the select box for categories categorySelect.id = 'categorySelect'; categorySelect.style.marginTop = '20px'; categorySelect.style.marginLeft = '20px'; // Add options to the select box dbOperations.initConversationData() .then(information => { controlPanelKit.updateCategorySelect(); }) .catch(error => console.error(error)); let allCategoriesString = translate("allCategoriesFilter"); let categories = [allCategoriesString]; for (let i = 0; i < chatHistory.length; i++) { let curChat = chatHistory[i]; if (curChat.categories) { for (let j = 0; j < curChat.categories.length; j++) { if (!categories.includes(curChat.categories[j])) categories.push(curChat.categories[j]); } } } categories.forEach(optionText => { let option = document.createElement('option'); option.text = optionText; categorySelect.appendChild(option); }); // Append the select box to the 'categoryEditor' div categoryEditor.appendChild(categorySelect); let translatedTitle = translate('conversationTitle'); let translatedOption = translate('actionOptions'); let translatedCategory = translate('conversationCategory'); let translatedTags = translate('conversationTags'); let panelHeader = document.createElement('div'); panelHeader.id = 'panelHeader'; // panelHeader.innerHTML = // '





  ${translatedTitle}  
操作
分类
标签





'; panelHeader.innerHTML = `





${translatedTitle}
${translatedOption}
${translatedCategory}
${translatedTags}





`; // Create the 'conversationContainer' div conversationContainer.id = 'conversationContainer'; conversationContainer.style.marginTop = '20px'; conversationContainer.style.overflow = 'auto'; conversationContainer.style.maxHeight = '700px'; // Append all child divs to the 'managePanel' div managePanel.appendChild(topicSearchContainer); managePanel.appendChild(categoryEditor); managePanel.appendChild(panelHeader); managePanel.appendChild(conversationContainer); // Append 'managePanel' to the document body or another container element document.body.appendChild(managePanel); right_Top_togglePanel.innerHTML = '×'; right_Top_togglePanel.style.position = 'fixed'; right_Top_togglePanel.style.fontSize = '30px'; right_Top_togglePanel.style.top = '20px'; right_Top_togglePanel.style.right = '20px'; right_Top_togglePanel.style.cursor = 'pointer'; managePanel.appendChild(right_Top_togglePanel); //controlPanelKit.renderConversations(chatHistory); controlPanelKit.addEventListeners(); }, updateCategorySelect: function () { log("inupdateSelect!"); categorySelect.innerHTML = ''; let allCategoriesString = translate("allCategoriesFilter"); let categories = [allCategoriesString]; for (let i = 0; i < chatHistory.length; i++) { let curChat = chatHistory[i]; if (curChat.categories) { for (let j = 0; j < curChat.categories.length; j++) { if (!categories.includes(curChat.categories[j])) categories.push(curChat.categories[j]); } } } categories.forEach(optionText => { let option = document.createElement('option'); option.text = optionText; categorySelect.appendChild(option); }); }, addEventListeners: function () { panelToggleButton.addEventListener('click', controlPanelKit.toggleHistoryPanel); searchTopicBox.addEventListener("input", controlPanelKit.executeFilter); categorySelect.addEventListener('change', controlPanelKit.executeFilter); right_Top_togglePanel.addEventListener('click', function () { managePanel.style.display = 'none'; if (states.visualization.thumbnailSvg === "visible") { panelToggleButton.style.display = 'block'; } }); }, executeFilter: function () { const selectedCategory = categorySelect.value.toLowerCase(); // 注意转换为小写 const query = searchTopicBox.value.toLowerCase(); filteredConversations = chatHistory.filter(conv => { return conv.topic.toLowerCase().includes(query); }); if (selectedCategory !== translate('allCategoriesFilter').toLowerCase()) { filteredConversations = filteredConversations.filter(conv => { return conv.categories.some(category => category.toLowerCase().includes(selectedCategory)); }); controlPanelKit.renderConversations(filteredConversations); log(`User selected category: ${selectedCategory}, filteredConversations:`, filteredConversations); } else { controlPanelKit.renderConversations(filteredConversations); log(`User selected category: ${selectedCategory}, filteredConversations:`, filteredConversations); } }, toggleHistoryPanel: function () { managePanel.style.display = 'block'; panelToggleButton.style.display = 'none'; }, renderConversations: function (conversations) { log("in_renderConv:", conversations); conversations.sort((a, b) => { // 比较isWholeConversationBookMarked字段 if (a.isWholeConversationBookMarked && !b.isWholeConversationBookMarked) { return -1; // a排在b前面 } if (!a.isWholeConversationBookMarked && b.isWholeConversationBookMarked) { return 1; // b排在a前面 } // 如果isWholeConversationBookMarked字段相同,则比较timestamp字段 if (a.timestamp > b.timestamp) { return -1; // a排在b前面 } if (a.timestamp < b.timestamp) { return 1; // b排在a前面 } return 0; // a和b相等,保持原来的顺序(稳定排序) }); // 现在chatHistory数组已经根据你的规则进行了排序。 //log("in render:"); const conversationContainer = document.getElementById("conversationContainer"); conversationContainer.innerHTML = ""; // Clear existing conversations let conversation_num = 0; conversations.forEach(conv => { conversation_num++; //const safeId = conv.id.replace(/-/g, ''); const safeId = conv.id; const convElem = document.createElement("div"); convElem.classList.add("conversation"); convElem.style.display = "flex"; convElem.style.justifyContent = "space-between"; // Create action buttons container const actionContainer = document.createElement("div"); actionContainer.classList.add("conversation-actions"); // Add topic const topicElem = document.createElement("h3"); topicElem.classList.add("topic"); const topicElem_2 = document.createElement("h3"); topicElem_2.innerText = conversation_num.toString() + ' '; topicElem.innerText = conv.topic; // 创建包含标题和按钮的一个容器 const topicContainer = document.createElement("div"); topicContainer.classList.add('topicContainer'); topicContainer.appendChild(topicElem_2); topicContainer.appendChild(topicElem); // 创建包含修改和删除按钮的一个容器 const optionsContainer = document.createElement("div"); optionsContainer.id = "optionsContainer_" + safeId; optionsContainer.classList.add("optionsContainer"); optionsContainer.innerHTML = conv.isWholeConversationBookMarked === false ? `
` : `
`; let buttons = optionsContainer.querySelectorAll('button'); for (let i = 0; i < buttons.length; i++) { buttons[i].style.cursor = 'pointer'; } buttons[0].addEventListener('click', function () { let topicElem = convElem.querySelector(".topic"); createTopicInput(conv, topicElem); }) buttons[1].addEventListener('click', function () { log("button1 clicked"); settingsKit.handleTwoTypesOfDeleteConversationData(conv.link, true) }) buttons[2].addEventListener('click', function () { window.open(conv.link, '_blank'); // let templink = document.createElement('a'); // templink.href = conv.link; // templink.target = '_blank'; // document.body.appendChild(templink); // templink.click(); // templink.remove(); }) buttons[3].addEventListener('click', function () { nodesInAndOutKit.copyToClipboard(conv.link); log("link_copied!"); }) buttons[4].addEventListener('click', function () { let seekURL = urlOperations.changeBetweenChattreeWithCAndOneMeansChattreeToC(0, conv.link); window.open(seekURL, '_blank'); log("deleted_information_to_load!"); }) buttons[5].addEventListener('click', function () { return handleBookMarks(conv, buttons[5]); }); async function handleBookMarks(conv, button) { //log("beforeClickIsBookMarked:", JSON.stringify(conv)); //await sleep(1000); const path = button.querySelector('path'); const currentFill = path.getAttribute('fill'); const newFill = currentFill === 'none' ? '#fe1616' : 'none'; path.setAttribute('fill', newFill); let shouldBeBookMarked = currentFill === 'none'; dbOperations.changeWholeConversationBookMarked(conv.link, shouldBeBookMarked); //conv.isWholeConversationBookMarked = currentFill === 'none'; // await sleep(1000); // log("afterClickIsBookMarked:", JSON.stringify(conv)); // await sleep(1000); // log("afterClickChatHistory:", JSON.stringify(chatHistory)); // await sleep(1000); // log("afterClickChatHistory:", JSON.stringify(filteredConversations)); } function createTopicInput(conv, replacedtopicElem) { const input = document.createElement("input"); input.type = "text"; input.value = replacedtopicElem.innerText; input.addEventListener('blur', function () { const value = input.value; if (value.trim()) { const topicElem = document.createElement("h3"); topicElem.innerText = value; conv.topic = value; topicElem.classList.add("topic"); topicContainer.appendChild(topicElem); dbOperations.changeConversationDataTopic(conv.link, value) } else { const topicElem = document.createElement("h3"); topicElem.innerText = conv.topic; topicElem.classList.add("topic"); topicContainer.appendChild(topicElem); } input.remove(); }); topicContainer.replaceChild(input, replacedtopicElem); input.focus(); } // 向 convElem 添加左和右容器 topicContainer.style.overflow = 'hidden'; convElem.appendChild(topicContainer); convElem.appendChild(optionsContainer); let categoriesContainer = document.createElement('div'); categoriesContainer.className = 'categoriesContainer'; categoriesContainer.id = "categoriesContainer_" + safeId; conv.categories.forEach(category => { // Add category const categoryElem = document.createElement("span"); categoryElem.classList.add("category"); categoryElem.innerText = `${category}`; // Add 'x' button for category const catDeleteBtn = document.createElement("span"); catDeleteBtn.innerHTML = "×"; catDeleteBtn.classList.add("delete-icon"); catDeleteBtn.onclick = function () { deleteClassOrTag("category", categoryElem, conv, category) // categoryElem.remove(); // conv.categories = conv.categories.filter(cat => cat !== category); }; categoryElem.appendChild(catDeleteBtn); categoriesContainer.appendChild(categoryElem); }); convElem.appendChild(categoriesContainer); // Add '+' button for adding new chatTreeTag or category const addClassBtn = document.createElement("span"); addClassBtn.innerHTML = "+"; addClassBtn.classList.add("add-icon"); addClassBtn.onclick = function () { createClassOrTagInput(categoriesContainer, "category", addClassBtn, conv); }; categoriesContainer.appendChild(addClassBtn); let tagContainers = document.createElement('div'); tagContainers.className = 'tagContainers'; tagContainers.id = "tagContainers_" + safeId; // Add chatTreeTags conv.chatTreeTags.forEach(chatTreeTag => { const chatTreeTagElem = document.createElement("span"); chatTreeTagElem.classList.add("chatTreeTag"); chatTreeTagElem.innerText = `${chatTreeTag}`; // Add 'x' button for chatTreeTag const chatTreeTagDeleteBtn = document.createElement("span"); chatTreeTagDeleteBtn.innerHTML = "×"; chatTreeTagDeleteBtn.classList.add("delete-icon"); chatTreeTagDeleteBtn.onclick = function () { deleteClassOrTag("chatTreeTag", chatTreeTagElem, conv, chatTreeTag) // chatTreeTagElem.remove(); // conv.chatTreeTags = conv.chatTreeTags.filter(tag => tag !== chatTreeTag); }; chatTreeTagElem.appendChild(chatTreeTagDeleteBtn); tagContainers.appendChild(chatTreeTagElem); }); convElem.appendChild(tagContainers); // Add '+' button for adding new chatTreeTag or category const addChatTreeTagBtn = document.createElement("span"); addChatTreeTagBtn.innerHTML = "+"; addChatTreeTagBtn.classList.add("add-icon"); addChatTreeTagBtn.onclick = function () { createClassOrTagInput(tagContainers, "chatTreeTag", addChatTreeTagBtn, conv); }; tagContainers.appendChild(addChatTreeTagBtn); function deleteClassOrTag(className, deletedTag, conv, value) { let operatingURL = conv.link; if (className === 'chatTreeTag') { dbOperations.addOrDeleteTagOrClassToURL(operatingURL, true, value, false); conv.chatTreeTags = conv.chatTreeTags.filter(tag => tag != value); } else { dbOperations.addOrDeleteTagOrClassToURL(operatingURL, false, value, false); conv.categories = conv.categories.filter(tag => tag != value); } deletedTag.remove(); } function createClassOrTagInput(convElem, className, replacedButton, conv) { // let safeId = convElem.id.split("_")[1]; // let foundObject = chatHistory.find(chat => chat.id === safeId); // // if (foundObject) { // log("Found the object:", foundObject); // } else { // log("Object not found"); // return; // } // log("conv:",conv); let operatingURL = conv.link; const input = document.createElement("input"); input.type = "text"; input.placeholder = `Enter new value`; convElem.replaceChild(input, replacedButton); input.focus(); input.addEventListener('blur', function () { const value = input.value; if (value.trim()) { // 创建新的类别或标签 const newElem = document.createElement("span"); newElem.classList.add(className); // 使用合适的类名 newElem.innerText = value; convElem.insertBefore(newElem, input); const catDeleteBtn = document.createElement("span"); catDeleteBtn.innerHTML = "×"; catDeleteBtn.classList.add("delete-icon"); newElem.appendChild(catDeleteBtn); if (className === 'chatTreeTag') { conv.chatTreeTags.push(value); dbOperations.addOrDeleteTagOrClassToURL(operatingURL, true, value, true); catDeleteBtn.onclick = function () { deleteClassOrTag(className, newElem, conv, value); }; } else { conv.categories.push(value); dbOperations.addOrDeleteTagOrClassToURL(operatingURL, false, value, true); catDeleteBtn.onclick = function () { deleteClassOrTag(className, newElem, conv, value); }; } } let addedBtn; if (className === 'chatTreeTag') { // Add '+' button for adding new chatTreeTag or category addedBtn = document.createElement("span"); addedBtn.innerHTML = "+"; addedBtn.classList.add("add-icon"); addedBtn.onclick = function () { createClassOrTagInput(convElem, "chatTreeTag", addedBtn, conv); } } else { // Add '+' button for adding new chatTreeTag or category addedBtn = document.createElement("span"); addedBtn.innerHTML = "+"; addedBtn.classList.add("add-icon"); addedBtn.onclick = function () { createClassOrTagInput(convElem, "category", addedBtn, conv); }; } if (input.nextSibling) { convElem.insertBefore(addedBtn, input.nextSibling); } else { convElem.appendChild(addedBtn); } input.remove(); }); } conversationContainer.appendChild(convElem); }); }, }; })();