// ==UserScript== // @name ChatGPT ChatTree 🌳 // @name:zh-CN ChatGPT ChatTree 🌳 // @name:es ChatGPT ChatTree 🌳 // @name:ar ChatGPT ChatTree 🌳 // @namespace https://czz9.top // @version 2025.04.25.04 // @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 *://chatgpt.com/* // @grant GM_addStyle // @grant GM_getResourceText // @resource languagepackage https://cdn.jsdelivr.net/gh/cuizhenzhi/ChatTree/lang.json // @resource css https://cdn.jsdelivr.net/gh/cuizhenzhi/ChatTree/index.css // @resource hljscss https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/default.min.css // @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://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.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 // @grant GM_xmlhttpRequest // @connect localhost // @connect analyze.chattree.cc // @connect *.chattree.cc // @connect chattree.cc // @connect ip-api.com // @icon data:image/svg+xml;utf8, // @downloadURL none // ==/UserScript== (async function (node) { // @require https://cdn.jsdelivr.net/gh/kudoai/chatgpt.js@516ad148b02335b98db82c89dec02e5fa28c7d56/dist/chatgpt-2.3.13.min.js "use strict"; const curVersion = '2025.04.25.04' function delay(duration){ return new Promise(resolve => { setTimeout(resolve,duration*1000) }) } const actype = [ 'useScript', 'updateCurrentConversationTree', 'toggleConversationTree', 'jumptonewnode', 'nodetakenote', 'nodebookmark', 'readManual', 'changeColors', 'searchForNode', 'changeLanguage', 'showNodeDetails' ] await delay(5) //chatgpt.logout(); console.log("chatgpt chattree!"); const isDevelopmentMode = true; // 定义支持的类别 const LogCategories = { //DEFAULT: 'DEFAULT', IMPORTANT: 'IMPORTANT', DEBUG: 'DEBUG', INFO: 'INFO', WARNING: 'WARNING', ERROR: 'ERROR', SUCCESS: 'SUCCESS', }; const LogStyles = { IMPORTANT: 'background-color: #007bff; color: white; padding: 8px; font-size: 16px;', //DEBUG: 'background-color: #f0f0f0; color: #333; border: 1px solid #ccc; padding: 5px;', INFO: 'background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; padding: 0px;', WARNING: 'background-color: yellow; color: black; border: 1px solid red; padding: 5px;', ERROR: 'color: red; font-weight: bold; font-size: 16px;', SUCCESS: 'background-color: green; color: white; border: 1px solid black;', } class Logger { constructor(moduleName) { this.moduleName = moduleName; this.log(LogCategories.SUCCESS, `${this.moduleName} logger created!`); } log(category = LogCategories.DEBUG, ...message) { // 发布模块信息 //console.log(`[${this.moduleName}] ${category}: ${message}`); let metadata = { moduleName: this.moduleName, category: category, style: LogStyles[category], //mesage: message, } let info = [metadata, ...message]; // 调用全局 log 函数 //console.log('Global log function, info:', info); log(...info); } } function log(...messages) { // 如果不是开发模式,则直接返回 if (!isDevelopmentMode) { return; } // 默认使用的类别 let defaultCategory = LogCategories.DEBUG; let defaultStyle = LogStyles.DEBUG; // 检查是否提供了类别参数 let category = defaultCategory; let style = defaultStyle; let metadata = {}; metadata.moduleName = 'undefined'; //console.log(messages[0], messages[0].category, LogCategories[messages[0].category]) if (messages.length > 0 && typeof messages[0] === 'object' && LogCategories[messages[0].category]) { metadata = messages.shift(); category = metadata.category; style = metadata.style; } if (LogCategories[messages[0]]) { category = messages[0]; style = LogStyles[category]; messages.shift(); } // 根据类别进行不同的处理 switch (category) { case LogCategories.IMPORTANT: console.log(`%c[IMPORTANT]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); // 这里可以添加额外的处理逻辑 break; case LogCategories.DEBUG: //console.debug(`%c[DEBUG]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); console.debug(`[DEBUG]\n ${metadata.moduleName}\n`, ...messages); break; case LogCategories.INFO: console.info(`%c[INFO]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); break; case LogCategories.WARNING: console.warn(`%c[WARNING]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); break; case LogCategories.ERROR: console.error(`%c[ERROR]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); break; case LogCategories.SUCCESS: console.log(`%c[SUCCESS]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg === 'object' ? JSON.stringify(msg) : msg).join('\n')}`, style); break; default: console.log(`[${category.toUpperCase()}]`, ...messages); } /* 以下输出方式: 如果不是字符串形式, 则正常输出 * // 根据类别进行不同的处理 switch (category) { case LogCategories.IMPORTANT: console.log(`%c[IMPORTANT]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg !== 'string' ? (console.log(msg), '') : msg).join('\n')}`, style); // 这里可以添加额外的处理逻辑 break; case LogCategories.DEBUG: console.debug(`[DEBUG]\n ${metadata.moduleName}`); messages.forEach(msg => { if (typeof msg !== 'string') { console.log(msg); } else { console.debug(msg); } }); break; case LogCategories.INFO: console.info(`%c[INFO]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg !== 'string' ? (console.log(msg), '') : msg).join('\n')}`, style); break; case LogCategories.WARNING: console.warn(`%c[WARNING]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg !== 'string' ? (console.log(msg), '') : msg).join('\n')}`, style); break; case LogCategories.ERROR: console.error(`%c[ERROR]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg !== 'string' ? (console.log(msg), '') : msg).join('\n')}`, style); break; case LogCategories.SUCCESS: console.log(`%c[SUCCESS]\n ${metadata.moduleName}\n ${messages.map(msg => typeof msg !== 'string' ? (console.log(msg), '') : msg).join('\n')}`, style); break; default: console.log(`[${category.toUpperCase()}]`, ...messages); } * */ } function isDark() { return document.documentElement.classList.contains('dark'); } GM_addStyle(GM_getResourceText("css")); // GM_addStyle(GM_getResourceText("hljscss")); const getLang = function (selectLang = null) { let lang = JSON.parse(GM_getResourceText("languagepackage")); const userLang = selectLang ? lang[selectLang] ? selectLang : 'en' : navigator.languages.find(l => l in lang) || 'en'; //const userLang = 'en'; //log("currentLang:",userLang); globalUserLang = userLang; return { langPack: lang[userLang], userLang: userLang } }; let globalUserLang; let currentLangPack= getLang().langPack; function translate(key) { return currentLangPack[key] || key; } const domain = 'https://chatgpt.com'; const endpoints = { assets: 'https://raw.githubusercontent.com/KudoAI/chatgpt.js/main', openAI: { session: `${domain}/api/auth/session`, chats: `${domain}/backend-api/conversations`, chat: `${domain}/backend-api/conversation`, share_create: `${domain}/backend-api/share/create`, share: `${domain}/backend-api/share`, instructions: `${domain}/backend-api/user_system_messages`, me: `${domain}/backend-api/me`, } }; let chatgpt = { openAISession: {}, getAccessToken: function () { return new Promise((resolve, reject) => { if (chatgpt.openAISession.accessToken) // not expired return resolve(chatgpt.openAISession.accessToken); const xhr = new XMLHttpRequest(); xhr.open('GET', endpoints.openAI.session, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve access token.'); chatgpt.openAISession = JSON.parse(xhr.responseText) return resolve(chatgpt.openAISession.accessToken); }; xhr.send(); }); } }; class ButtonCreator { static createButton(options = {}) { const { id, text, innerHTML, eventListeners = [], additionalStyles = {}, } = options; const button = document.createElement('button'); button.id = id; button.textContent = text; if (!button.textContent) button.innerHTML = innerHTML; eventListeners.forEach(({type, handler}) => { button.addEventListener(type, handler); }); Object.assign(button.style, additionalStyles); return button; } } let DEFAULT_USER_TOP_COLOR = "#ff7f00", DEFAULT_USER_BOTTOM_COLOR = "#ffc085", DEFAULT_CHATGPT_TOP_COLOR = "#0a87d8", DEFAULT_CHATGPT_BOTTOM_COLOR = "#34aeeb"; let GPT_Avatar_Config = { gpt4_Inner_Html: "
", gpt3_Inner_Html: "", } const states = { mainButton: { isDragging: false, // isMouseOver: false, // isThereNavbar: false, //用来定义是否存在历史记录栏 isMouseInNavbar: false, //用来定义拖动mainbtn时鼠标是否在navbar内 isMainTreeBtnInNavbar: false, //历史记录栏里的treemainbtn是否显示 , 用来定义navbar的mainbtn的可见性 canNotEnterNavbar: false //小圣诞树是绿色/红色, 用来定义maintreeBtn的固定性 }, url: { isForLiveValidURL: false, isForDeletedValidURL: false, url: '' }, treeUpdate: { isDOMOperating: false, }, visualization: { contentDiv: 0, thumbnailSvg: 0, panelToggleButton: 0, }, colorSetting: { customUser: { is: false, url: "url(#CUSTOM_USER_GRADIENT)", top: DEFAULT_USER_TOP_COLOR, bottom: DEFAULT_USER_BOTTOM_COLOR, }, customChatGPT: { is: false, url: "url(#CUSTOM_CHATGPT_GRADIENT)", top: DEFAULT_CHATGPT_TOP_COLOR, bottom: DEFAULT_CHATGPT_BOTTOM_COLOR, }, }, }; const selector = { gptContentDiv: "div[data-message-author-role='assistant']", userContentDiv: "div[data-message-author-role='user']", fowardBackwardButton: ".flex.items-center.justify-center.text-token-text-secondary", branchesInfo: ".text-sm.font-semibold.tabular-nums", mainNavbar: '.flex.h-full.w-full.flex-col.px-3',//".flex-shrink-0.overflow-x-hidden",//侧边栏里有style属性的那个div, 用来确定是否有必要判断maintreebtn从移动到固定 navbarSize: '.flex.h-full.min-h-0.flex-col',//侧边栏的**实际大小和边缘监测**用的div, historyDiv: '.flex.flex-col.gap-2.pb-2.text-token-text-primary.text-sm.mt-5'//'.flex-col.flex-1.transition-opacity.duration-500.-mr-2.pr-2.overflow-y-auto', }; const Default_RootNode_Content_2 = { "content_type": "text", "parts": [ "CHAT STARTS HERE" ], "author": { "role": "chatGPT", "metadata": {} }, "metadata": { "finish_details": { "type": "stop", }, "is_complete": true, "timestamp_": "absolute" }, "recipient": "all", "status": "finished_successfully", "weight": 1 }; 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'; const CONVERSATION_NODE_POSITION_STORE_NAME = 'conversationNodePositions'; const CONVERSATION_NODE_POSITION_NAMELIST_NAME = "chattreenames"; let db; let zoomIdentity_translatex = 0, zoomIdentity_translatey = 0, transformscale = 1; class DialogueNode { constructor(content, type, uuid = -1, parent = -1) { this.content = content; this.type = type; this.uuid = uuid;// === -1 ? generateUUID() : uuid; this.children = []; this.comment = ''; this.parent = parent; // this.content_type = content_type; } } let USER_Avatar_Config = { USER_DEFAULT_HTML: "" } let DEFAULT_CONVERSATION_DATA = { rootNode: new DialogueNode(Default_RootNode_Content, "chatGPT"), uuid2nodeMap: new Map(), bookMarked: new Map(), commentMap: new Map(), timestamp: Date.now(), isNovemberSeventh: true, isWholeConversationBookMarked: false, categories: [], chatTreeTags: [], participants: { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "User", }, gpt: { type: "GPT-3", } } }; let conversationData = DEFAULT_CONVERSATION_DATA; let rawConversationData = {}; let root, svg, svgThumbnail, defs, gLinks, gNodes, nodeDrag, canvasDrag, zoom, searchHistoryRecord, chatHistory = [], curMouseOnUUID = null; const treeLayout = d3.tree().nodeSize([30, 30]); const defaultTransform = { // id: this.conversationData.rootNode.uuid +name, zoomIdentity_translatex: 0, zoomIdentity_translatey: 0, transformscale: 1 } let curTreeName = "default"; const defaultTreeName = "default" let globalShortCut = { togglechattree: "", updatechattree: "" } let shortCutMonitoring = true; const urlOperations = { log(category, ...message) { this.logger.log(category, ...message); }, initUrlOperations: function () { this.logger = new Logger('urlOperations'); }, getCurrentURL: function () { return window.location.href; }, isForLiveValidURL: function (url) { // const pattern = /^https:\/\/chatgpt\.com\/c\/[a-z0-9\-]+\/?$/; // // const pattern2 = /^https:\/\/chatgpt\.com\/g\/[a-zA-Z0-9\-]+\/c\/[0-9a-z\-]+$/; const pattern = /^https:\/\/chatgpt\.com\/c\/[a-z0-9\-]+\/?(?:\?.*)?$/; const pattern2 = /^https:\/\/chatgpt\.com\/g\/[A-Za-z0-9\-]+\/c\/[0-9a-z\-]+\/?(?:\?.*)?$/; return !!pattern.test(url) || !!pattern2.test(url); }, isForDeletedValidURL: function (url) { const pattern = /^https:\/\/chatgpt\.com\/chattree\/[a-z0-9\-]+\/?$/; return pattern.test(url); }, changeBetweenChattreeWithCAndOneMeansChattreeToC: function (chattreetoc, url) { function replaceFirstChattreeWithC(url) { return url.replace(/^https:\/\/chatgpt\.com\/chattree/, `${domain}/c`); } function replaceFirstCWithChattree(url) { return url.replace(/^https:\/\/chatgpt\.com\/c/, `${domain}/chattree`); } if (chattreetoc) { return replaceFirstChattreeWithC(url); } else { return replaceFirstCWithChattree(url); } }, isNonUniqueURL: function (url) { const nonUniquePatterns = [ /^https:\/\/chatgpt\.com\/?$/, /^https:\/\/chatgpt\.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; this.log(LogCategories.INFO, "fistURL:", lastURL); if (urlOperations.isForLiveValidURL(lastURL) || urlOperations.isForDeletedValidURL(lastURL)) { //log("is_anyKind_of_valid"); urlOperations.handleURLChange(lastURL); states.url.url = lastURL; } const self = this; function callback(mutationsList, observer) { const currentURL = window.location.href; //log("currentURL:", currentURL); if (urlOperations.isForLiveValidURL(currentURL)) { if (currentURL !== lastURL) { self.log(LogCategories.INFO, "URL changed:", currentURL); lastURL = currentURL; urlOperations.handleURLChange(currentURL); states.treeUpdate.isDOMOperating = false; } else { //this.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)) { self.log(LogCategories.WARNING, "Non-unique URL:", currentURL, " detected. Please refresh the page."); urlOperations.handleURLChange(currentURL); } }; setInterval(() => { callback(); //console.log("Current URL:", window.location.href); }, 2000); }, handleURLChange: function (url) { //log("In handleURLChange, Data:", conversationData); if (urlOperations.isNonUniqueURL(url)) { this.log(LogCategories.WARNING, "nonuniqueURL: ", url); states.url.isForLiveValidURL = false; states.url.isForDeletedValidURL = false; states.url.url = ''; states.treeUpdate.isDOMOperating = false; conversationData = DEFAULT_CONVERSATION_DATA; root = d3.hierarchy(conversationData.rootNode); treeLayout(root); settingsKit.refreshTree(); //this.log("请刷新页面或者转到具有对话信息的页面从而获取正确的链接"); } else if (urlOperations.isForDeletedValidURL(url)) { //this.log(LogCategories.INFO, "DeletedValidURL"); 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) url = urlOperations.changeBetweenChattreeWithCAndOneMeansChattreeToC(1, url); //log("delete_url_to_new_url", url); dbOperations.loadConversationsData(url).then(loadeddata => { this.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); treeLayout(root); settingsKit.refreshTree(); }).catch(error => { console.error("Error loading data:", error); }); } else { this.log(LogCategories.INFO, "liveValidURL"); states.url.isForLiveValidURL = true; states.url.isForDeletedValidURL = false; states.treeUpdate.isDOMOperating = false; states.url.url = url; const regex = /\/c\/([0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})/; const m = url.match(regex); if (m) console.log(m[1]); else return; fetchRawChatMessages(m[1]).then(data => { dbOperations.loadConversationsData(url).then(loadeddata => { this.log("Loaded data for URL:", loadeddata); conversationData = loadeddata; if (!conversationData.isNovemberSeventh) { let result = conver_to_new_style(); conversationData.bookMarked = result.bookMarked; conversationData.commentMap = result.commentMap; //delete conversationData.uuid2pathMap; delete conversationData.path2nodeMap; dbOperations.saveConversationsData(conversationData).then(() => { this.log("Convert To November_Type!"); }); } this.log("RAW DATA FROM OPENAI:", data); rawConversationData = data; //log("New URL Catched!"); processChatMessage(data); dbOperations.saveConversationsData(conversationData).then(() => { controlPanelKit.renderConversations(chatHistory); controlPanelKit.updateCategorySelect(); }); //log("to draw svg:", conversationData); //conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); treeLayout(root); restoreAllNodePositions(db, root.descendants()) }).catch(error => { console.error("Error loading data:", error); }); }).catch(error => { this.log(LogCategories.ERROR, "error:", error); }) } } }; function restoreAllNodePositions(db, nodes, key = conversationData.rootNode.uuid) { const transaction = db.transaction([CONVERSATION_NODE_POSITION_STORE_NAME], "readonly"); const store = transaction.objectStore(CONVERSATION_NODE_POSITION_STORE_NAME); let request = store.get(key); // 根据 uuid 获取节点位置数据 request.onsuccess = function() { const data = request.result; console.log('positiondata: ',data) let nodecount = 0; if(!data){ settingsKit.refreshTree(); return; } nodes.forEach(node => { let nodedata = data[node.data.uuid] if (nodedata) { nodecount ++; node.x = nodedata.x; // 更新节点的 x 坐标 node.y = nodedata.y; // 更新节点的 y 坐标 // console.log(`Position restored for node ${node.data.uuid}`); } else { console.log(`No data found for node ${node.data.uuid}`); } }); if(nodecount === 0) { settingsKit.refreshTree(); }else { drawMainSVG(); } }; request.onerror = function(event) { console.error(`Error fetching position for node ${node.data.uuid}: ${event.target.error.message}`); }; transaction.oncomplete = function() { console.log("All node positions have been restored from the database."); }; transaction.onerror = function(event) { console.error("Failed to restore node positions: ", transaction.error); }; } urlOperations.initUrlOperations(); //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: {id: 'mainTreeBtn', color: '#0FD126', opacity: 0.9}, MainTreeBtnPositionSettings: {id: 'mainTreeBtnPos', top: '20px', left: '800px'}, mainTreeBtnSticky: {id: 'mainTreeBtnSticky', canNotEnterNavbar: false}, MainSVGBackground: DEFAULT_MAINSVG_BACKGROUND, }; const dbOperations = { log(category, ...message) { // 使用 Logger 实例的 log 方法 //console.log("moduleLog called."); this.logger.log(category, ...message); }, initDatabase: function () { this.logger = new Logger('dbOperations'); //this.log(LogCategories.SUCCESS, 'DBLoggerCreated!'); return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 2); request.onerror = event => { reject("Error opening database:", event); }; request.onupgradeneeded = event => { this.log(LogCategories.INFO, "After LOADING db: ", db); this.log(LogCategories.INFO, "升级数据库!"); 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', right: '20px'}; userSettingsStore.add(mainTreeBtnPosSettings); const mainTreeBtnSticky = {id: 'mainTreeBtnSticky', canNotEnterNavbar: false}; userSettingsStore.add(mainTreeBtnSticky); } if (! db.objectStoreNames.contains(CONVERSATION_NODE_POSITION_STORE_NAME)) { db.createObjectStore(CONVERSATION_NODE_POSITION_STORE_NAME, { keyPath: "id" }); } }; request.onsuccess = event => { db = event.target.result; resolve(); }; }); }, usedatabase: function () { dbOperations.getSearchHistory() .then(records => { this.log(LogCategories.INFO, '获取到搜索历史记录:', records); }) .then(() => dbOperations.getUserSettings('mainTreeBtn')) .then(mainTreeBtnSettings => { this.log(LogCategories.INFO, '获取到主树按钮设置:', 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'; }) .then(() => dbOperations.getUserSettings('mainTreeBtnPos')) .then(mainTreeBtnPos => { this.log(LogCategories.INFO, '获取到主树按钮位置设置:', 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; } }) .then(() => dbOperations.getUserSettings('userLang')) .then(userLang => { languageSelect.addEventListener('change', languageKits.init); this.log(LogCategories.INFO, '获取到用户语言设置:', userLang); if (userLang && userLang.globalUserLang) { globalUserLang = userLang.globalUserLang; } ButtonOperations.showUserNotification(translate("chatTreeRunning")); const options = languageSelect.querySelectorAll('option'); options.forEach(option => { option.selected = option.value === globalUserLang; }); }) .then(() => dbOperations.getUserSettings('mainSVG')) .then(mainSVGBackground => { this.log(LogCategories.INFO, '获取到主SVG背景设置:', mainSVGBackground); if (mainSVGBackground && mainSVGBackground.background) { GlobalUserSettings.MainSVGBackground = mainSVGBackground.background; let mainSvg = document.getElementById('mainSvg'); if (mainSvg) { mainSvg.style.background = mainSVGBackground.background.toString(); } } }) .then(() => dbOperations.getUserSettings('userColor')) .then(userColor => { this.log(LogCategories.INFO, '获取到用户颜色设置:', userColor); if (userColor && userColor.state && userColor.state.is) { initSvgAndGradient.createLinearGradient(defs, "CUSTOM_USER_GRADIENT", userColor.state.bottom, userColor.state.top); states.colorSetting.customUser = userColor.state; states.colorSetting.customUser.url = 'url(#CUSTOM_USER_GRADIENT)'; } }) .then(() => dbOperations.getUserSettings('chatGPTColor')) .then(chatGPTColor => { this.log(LogCategories.INFO, '获取到ChatGPT颜色设置:', chatGPTColor); if (chatGPTColor && chatGPTColor.state && chatGPTColor.state.is) { initSvgAndGradient.createLinearGradient(defs, "CUSTOM_CHATGPT_GRADIENT", chatGPTColor.state.bottom, chatGPTColor.state.top); states.colorSetting.customChatGPT = chatGPTColor.state; states.colorSetting.customChatGPT.url = 'url(#CUSTOM_CHATGPT_GRADIENT)'; } }) // .then(drawMainSVG) .then(() => dbOperations.getUserSettings('mainTreeBtnSticky')) .then(mainTreeBtnSticky => { console.log("mainTreeBtnSticky: ", mainTreeBtnSticky); if (mainTreeBtnSticky.id) { states.mainButton.canNotEnterNavbar = mainTreeBtnSticky.canNotEnterNavbar; toggleMainBtnMovingAccessbility.innerHTML = mainTreeBtnSticky.canNotEnterNavbar ? greenForNotEnter : redForEnterable; // treeMainBtn.style.display = mainTreeBtnSticky.canNotEnterNavbar ? 'none' : 'block'; // navMainButton.style.display = mainTreeBtnSticky.canNotEnterNavbar ? 'block' : 'none'; } else { throw new Error("从getUserSettings获取的结构不符合预期"); } }) .catch(error => { console.error(error); toggleMainBtnMovingAccessbility.innerHTML = redForEnterable; }) .then(() => dbOperations.initConversationData()) .then(information => { this.log(LogCategories.INFO, "获取到对话数据:", information); controlPanelKit.renderConversations(chatHistory); controlPanelKit.updateCategorySelect(); filteredConversations = chatHistory; }) .catch(error => console.error(error)); }, 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(this.log(LogCategories.WARNING, "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 => { this.log(LogCategories.SUCCESS, "Information updated successfully."); resolve(information); }; putRequest.onerror = event => { console.error("Failed to update information.", event.target.error); reject(event.target.error); }; } chatHistory = information.conversations; //log("information.conversations:",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) { 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; //this.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(); this.log(LogCategories.INFO, "In DBOper:", "url:", url, "isTag:", isTag, "value:", value, "isAdd:", isAdd); }) .catch(error => { console.error(error); }); }; request.onerror = event => reject("Error loading data:", event.target.errorCode); }); }, saveConversationsData: function (data) { if (data.url === null) { this.log(LogCategories.ERROR, "保存对话数据: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); this.log(LogCategories.INFO, "in save data, conversations:", objectStore); const request = objectStore.put(data); request.onsuccess = () => { dbOperations.initConversationData().then(() => { this.log(LogCategories.INFO, "update panel."); }) 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; //this.log(LogCategories.SUCCESS, 'Got USER SETTING:', settings); resolve(settings); }; request.onerror = function (event) { this.log(LogCategories.ERROR, '读取设置失败', event.target.errorCode); reject(event.target.errorCode); }; }); }, updateUserSettings: function (newSettings) { let self = this; return new Promise((resolve, reject) => { //console.log("db:",db); 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) { self.log(LogCategories.SUCCESS, '设置更新成功'); resolve(); }; request.onerror = function (event) { self.log(LogCategories.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; //this.log(" resultOfRequest:", result); if (!result) { let conversationData = DEFAULT_CONVERSATION_DATA; conversationData.url = url; //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 { conversationData = result; this.log("loaded conversationData:", conversationData); if (!conversationData.bookMarked) { conversationData.bookMarked = new Map(); } if (!conversationData.commentMap) { conversationData.commentMap = new Map(); } if (!conversationData.participants) { conversationData.participants = { user: { name: "UserName", avatarURL: "UserAvatarURL", avatarHTML: "User", }, gpt: { type: "GPT-3", } } } 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 = () => { this.log(LogCategories.WARNING, `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 = () => { this.log(LogCategories.SUCCESS, '搜索记录已成功更新'); resolve(); }; request.onerror = () => { this.log(LogCategories.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 || []; this.log(LogCategories.SUCCESS, '搜索历史记录:', records); searchHistoryRecord = records; resolve(records); }; request.onerror = event => { console.error('获取搜索历史记录失败'); reject(new Error('获取搜索历史记录失败')); }; }); } }; const titleDiv = document.createElement('div'); const title = document.createElement('h3'); const stickyDiv = document.createElement('div'); let navPanelButton; let navMainButton; const DOMOperations = { log(category, ...message) { // 使用 Logger 实例的 log 方法 //console.log("moduleLog called."); this.logger.log(category, ...message); }, initDOMOperations: function () { this.logger = new Logger('DOMOperations'); }, jumpToDialogueItem: async function (uuid) { analyzeKit.uploadActivityLog(3) function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } if (!states.url.isForLiveValidURL) { return; } if (!uuid || !conversationData.uuid2pathMap.get(uuid) || uuid === conversationData.rootNode.uuid) { return; } let path = conversationData.uuid2pathMap.get(uuid); let content = conversationData.uuid2nodeMap.get(uuid); if (!path) { log("没有从map中获取路径! 请检查!") return; } const wait_after_click = 50 let result = await DOMOperations.getButtonInfo(); // log("path:", path); // console.log('jumpToDialogueItem',result,) let i = 0; while (i < path.length) { result = await DOMOperations.getButtonInfo(); if (path[i] === result.childIndicesPath[i]) { i++; continue; } if (path[i] < result.childIndicesPath[i]) { if (result.childIndicesPath[i] === 0) { confirm("Due to potential issues with the chatgpt page, the previously saved path might be outdated or untraceable. To retrieve this path, please refresh the entire tree via the central ChatTree button. If you wish to perform a complete tree update in order to get the accurate path, enter 'Y' as instructed in the previous prompt.") return; } DOMOperations.clickButtonAtDivLevel(0, i); log("path:", path); result = await DOMOperations.getButtonInfo(); await sleep(wait_after_click); continue; } if (path[i] > result.childIndicesPath[i]) { if (result.childIndicesPath[i] === result.childrenCountPath[i]) { console.log('confirm result: ',result) confirm("Due to potential issues with the chatgpt page, the previously saved path might be outdated or untraceable. To retrieve this path, please refresh the entire tree via the central ChatTree button. If you wish to perform a complete tree update in order to get the accurate path, enter 'Y' as instructed in the previous prompt.") return; } DOMOperations.clickButtonAtDivLevel(1, i); log("path:", path); result = await DOMOperations.getButtonInfo(); await sleep(wait_after_click); continue; } } let j = 0; for (; j < path.length; j++) { if (path[j] !== result.childIndicesPath[j]) { log("在:", j, "没有成功转到!"); log(path, result.childIndicesPath); break; } } await 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]); //if content 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: 'nearest'}); const dark = isDark(); if (dark) { highlightDiv1(); } else { highlightDiv2(); } setTimeout(() => { allDivs[path.length - 1].scrollIntoView({behavior: 'smooth', block: 'center', inline: 'nearest'}); const dark = isDark() if (dark) { highlightDiv1(); } else { highlightDiv2(); } }, 1000); //log("成功转到!"); } }, setNavBarDiv: function () { console.log("setNavBarDiv") const tryLocateSidebar = () => { let HistoryDiv = document.querySelector(selector.historyDiv); if (HistoryDiv) { // 如果找到侧边栏,则执行相应操作 if (HistoryDiv.parentNode) { HistoryDiv.parentNode.insertBefore(stickyDiv, HistoryDiv); navPanelButton.parentNode.insertBefore(navMainButton, navPanelButton); HistoryDiv.parentNode.insertBefore(titleDiv, stickyDiv); } console.log("setNavBarDiv",HistoryDiv, stickyDiv, navMainButton, navPanelButton, titleDiv) return true; // 返回 true 表示已经成功定位侧边栏 } else { console.log("setNavBarDiv",HistoryDiv, stickyDiv, navMainButton, navPanelButton, titleDiv) // 如果未找到侧边栏,则继续尝试 return false; // 返回 false 表示侧边栏未定位 } }; let try_count = 0; const intervalId = setInterval(() => { const isAdded = tryLocateSidebar(); try_count++; if (isAdded) { clearInterval(intervalId); // 如果成功定位到侧边栏,则停止定时器 } else { if(try_count === 5){ clearInterval(intervalId); // 如果成功定位到侧边栏,则停止定时器 return; } this.log("侧边栏尚未定位,继续尝试..."); } }, 1000); // 每隔 1 秒尝试一次 // 返回 intervalId 以便在需要时能够停止定时器 return intervalId; }, getAllDivs: function () { // log("allDivs:",document.querySelectorAll(selector.allDivs)); // let all_divs = document.querySelectorAll(selector.allDivs); const selector_all_divs = "article.w-full.text-token-text-primary"; const selector_all_divs_filter = 'button.relative.w-full.text-start.inline' const all_divs = document.querySelectorAll(`${selector_all_divs}:not(:has(${selector_all_divs_filter}))`); // let all_divs = Array.from(document.body.querySelectorAll('article')) //all_divs[27].querySelector('button.relative.w-full.text-start.inline') // all_divs = all_divs.filter(i=> !i.querySelector('button.relative.w-full.text-start.inline')) all_divs.forEach(i=>{ let el =i.querySelector('.relative.flex-col.gap-1'); function triggerShowButtonInfos(){ // 这里的代码跑在页面本身的环境里…… // 如果页面也监听了 PointerEvent(pointerover / pointermove),再补一下: el.dispatchEvent(new PointerEvent('pointerover', { bubbles: true, cancelable: true, pointerType: 'mouse', clientX: 10, clientY: 10 })); } triggerShowButtonInfos(); }) return all_divs; }, clickButtonAtDivLevel: function (buttonIndex, divLevel = -1,) { let allChatDivs = DOMOperations.getAllDivs(); if (divLevel >= allChatDivs.length) { return; } let conversationDiv = allChatDivs[divLevel]; const buttonInfoDiv = conversationDiv.querySelector(selector.fowardBackwardButton); //log("buttonInfo:",buttonInfoDiv); let buttons = buttonInfoDiv.querySelectorAll("button"); if (buttons) { //log("In clickButtonAtDivLevel", "divLevel: ", divLevel, "buttons:", buttons); buttons[buttonIndex].click(); } else { this.log(LogCategories.ERROR, "未能找到应点击的Div"); } }, getButtonInfo: async function () { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } //this.log("In getButtonInfo!"); let hasRightButtonUnClicked = false; let hasLeftButtonUnClicked = false; let allChatDivs = DOMOperations.getAllDivs(); // console.log(allChatDivs,) await sleep(10); 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(selector.fowardBackwardButton); // if(i === 14) // console.log(i, ' buttonInfo Div',div,buttonInfoDiv) if (buttonInfoDiv) { let span = buttonInfoDiv.querySelector(selector.branchesInfo); 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 { // console.log(i, 'no buttonInfo Div',div) 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(selector.gptContentDiv); if (isGPT) { isGPT = div.querySelector(selector.gptContentDiv); } else { isUser = div.querySelector(selector.userContentDiv); } 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 keyToArray(str) { const numArray = str.split('|').map(function (item) { return parseInt(item, 10); }); } 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 toggleSvgShowFromButton(){ toggleSvgShow(1) } 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 (navMainButton.style.display === 'block') { 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 mainBtnColorPicking = false; let offsetX, offsetY; let menu; let mainBtnColorPicker, mainBtnOpacityPicker; const ButtonOperations = { createButton: function () { treeMainBtn = ButtonCreator.createButton({ id: 'chatTreeBtn', text: '🌳ChatTree🌳', eventListeners: [ {type: 'mouseenter', handler: () => ButtonOperations.showMenu(0)}, {type: 'mouseleave', handler: ButtonOperations.hideMenuIfNotOver}, {type: 'mousedown', handler: ButtonOperations.onMouseDown}, {type: 'click', handler: ButtonOperations.onClick}, ], additionalStyles: { display: 'block', position: 'fixed', zIndex: '9999', resize: 'both', width: '150px', color: 'white', height: '30px', backgroundColor: GlobalUserSettings.MainTreeBtnColorSettings ? GlobalUserSettings.MainTreeBtnColorSettings.color : 'rgb(16, 209, 38)', opacity: GlobalUserSettings.MainTreeBtnColorSettings ? GlobalUserSettings.MainTreeBtnColorSettings.opacity : '0.9', borderRadius: '12px', // ... 添加其他样式 }, }); try { treeMainBtn.style.left = GlobalUserSettings.MainTreeBtnPositionSettings.left ? GlobalUserSettings.MainTreeBtnPositionSettings.left : '300px'; treeMainBtn.style.top = GlobalUserSettings.MainTreeBtnPositionSettings.top ? GlobalUserSettings.MainTreeBtnPositionSettings.top : '20px'; } catch (e) { treeMainBtn.style.right = '30%'; treeMainBtn.style.top = '20px'; } document.body.appendChild(treeMainBtn); 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-2', 'text-xs', 'font-medium', 'text-ellipsis', 'overflow-hidden', 'break-all', 'bg-white', 'dark:bg-black', 'text-gizmo-gray-600'); title.textContent = '🌳ChatGPT • ChatTree🌳'; titleDiv.appendChild(title); stickyDiv.style.flexDirection = 'column'; stickyDiv.style.alignItems = 'stretch'; stickyDiv.style.display = 'flex'; navPanelButton = ButtonCreator.createButton({ id: 'navPanelButton', text: translate("openAdminPanel"), additionalStyles: { display: 'block', borderRadius: '12px', opacity: '0.9', background: 'linear-gradient(to right, rgb(0, 123, 255), rgb(0, 198, 255))', color: 'white', padding: '4px', fontWeight: 'bold', boxShadow: 'rgba(0, 0, 0, 0.2) 0px 3px 5px', margin: '6px', height: '30px', }, eventListeners: [ {type: 'click', handler: controlPanelKit.toggleHistoryPanel}, ], }); stickyDiv.appendChild(navPanelButton); navMainButton = ButtonCreator.createButton({ id: 'navMainButton', text: '🌳ChatTree🌳', eventListeners: [ {type: 'mouseenter', handler: () => ButtonOperations.showMenu(1)}, {type: 'mouseleave', handler: ButtonOperations.hideMenuIfNotOver}, {type: 'mousedown', handler: ButtonOperations.onMouseDown}, {type: 'click', handler: ButtonOperations.onClick}, ], additionalStyles: { display: 'none', zIndex: '9999', resize: 'both', height: '30px', borderRadius: '12px', margin: '6px', right: 'auto', bottom: 'auto', color: 'white', padding: '4px', fontWeight: 'bold', boxShadow: 'rgba(0, 0, 0, 0.2) 0px 3px 5px', }, }); }, toggleStickyMainBtn: function () { //log("rightClick!"); if (states.mainButton.canNotEnterNavbar) {//要可能被固定 states.mainButton.canNotEnterNavbar = false; toggleMainBtnMovingAccessbility.innerHTML = redForEnterable; ButtonOperations.showUserNotification("ChatTree now can be fixed in the nav bar!") } else {//要变得可以到处移动 states.mainButton.canNotEnterNavbar = true; navMainButton.style.display = 'none'; toggleMainBtnMovingAccessbility.innerHTML = greenForNotEnter; states.mainButton.isMainTreeBtnInNavbar = false; if (treeMainBtn.style.display === 'none') {//如果此时不见了, 要先展示出来. 如果此时本来就block, 就不动 treeMainBtn.style.left = window.innerWidth - 300 + 'px'; treeMainBtn.style.top = '20px'; treeMainBtn.style.display = 'block'; } ButtonOperations.showUserNotification("ChatTree Now Can Move AnyWhere!") } const newSettings = {id: 'mainTreeBtnSticky', canNotEnterNavbar: states.mainButton.canNotEnterNavbar}; dbOperations.updateUserSettings(newSettings).then(() => { }).catch(error => { console.error("Error saving Change:", error); }); log("newSettings:", newSettings); ButtonOperations.onMouseUp(); }, onMouseDown: function (e) { //console.log("target:",e.target); 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(selector.mainNavbar);//这里原来使用的是selector.navbarSize, 但是貌似可以直接用mainNavbar来复用就好了 let mainNavBar = document.querySelector(selector.mainNavbar) // let mainNavBarVisible = "hidden"; // if (mainNavBar){ // mainNavBarVisible = mainNavBar.style.visibility; // //log("navbarVisible:",mainNavBarVisible, "navmain:",mainNavBar); // } let mainSvgDiv = document.getElementById("mainSvgDiv") states.mainButton.isThereNavbar = !!mainNavBar; if (states.mainButton.isThereNavbar) { if (mainNavBar.style.visibility !== 'hidden' && mainSvgDiv.style.display === 'none') { let navbarRect = mainNavBar.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 && !states.mainButton.canNotEnterNavbar) { 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 === false) { 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(); } if (menu) { 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 () { if (mainBtnColorPicking) { return; } isMouseOver = false; setTimeout(() => { if (!isMouseOver) ButtonOperations.hideMenu(); }, 100); }, hideMenu: function () { if (menu) { document.body.removeChild(menu); menu = null; } }, updateTree(){ analyzeKit.uploadActivityLog(1) 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; } //console.log("states.url.url: ",states.url.url); setTimeout(() => { fetchRawChatMessages(conversationData.url.slice(-36)).then(data => { rawConversationData = data; processChatMessage(data); dbOperations.saveConversationsData(conversationData).then(() => { controlPanelKit.renderConversations(chatHistory); controlPanelKit.updateCategorySelect(); }); log("to draw svg:", conversationData); //conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); treeLayout(root); restoreAllNodePositions(db, root.descendants()) // settingsKit.refreshTree(); // drawMainSVG(); }).catch(error => { log("error:", error); }) }, 100); } else { log("按钮点击而开始更新树!但是条件不允许!states:", states); } }, onMenuClick: function (e) { if (e.target.id === 'adjustOption') { analyzeKit.uploadActivityLog(7) mainBtnColorPicker.style.display = 'block'; mainBtnOpacityPicker.style.display = 'inline-block'; mainBtnColorPicking = true; } if (e.target.id === 'opt_updateTree') { ButtonOperations.updateTree(); } if (e.target.id === 'showSvg') { analyzeKit.uploadActivityLog(2) toggleSvgShow(1); } if (e.target.id === 'downloadConversation'){ // console.log('downloadConversation!') ButtonOperations.processDownloadConversation(); } if (e.target.id === 'jumpToManual'){ // console.log('downloadConversation!') analyzeKit.uploadActivityLog(6) ButtonOperations.jumpToManual(); } if(e.target.id === 'setshortCuts'){ let shortCutdiv = document.getElementById("shortCutdiv"); shortCutdiv.style.display = "block"; shortCutMonitoring = false; } if(e.target.id === 'supportMeChattree'){ console.log("supportMe") } if (e.target.id === 'adjustOption' || e.target.id === 'opt_updateTree' || e.target.id === 'showSvg' || e.target.id === 'downloadConversation') ButtonOperations.showUserNotification(translate("selectedItem").replace('{item}', e.target.innerText)); }, currentMessages: 0, showUserNotification: function (text, color = "green", duration = 3000, options) { const message = document.createElement("div"); const innerHtml = (color !== "blue") ? ``; // }); const codeBlocks = []; const codecolor = isDark() ? 'white': 'black' markdown = markdown.replace(/(?:\n\n|^)```(\w+)?\s*([\s\S]*?)\n```(?:\n\n)/gm, function (match, lang, code, index) { lang = lang || 'plaintext'; const escapedCode = code.replace(//g, '>'); const codeHtml = `${lang}${escapedCode} //
`; // Push the current code block's HTML to the array and replace with a placeholder codeBlocks.push(codeHtml); return `CODEBLOCK${codeBlocks.length - 1}\n`; }); markdown = markdown.replace(/(?:\n\n|^)```(\w+)?\s*([\s\S]*)$/, function (match, lang, code, index) { lang = lang || 'plaintext'; const escapedCode = code.replace(//g, '>'); const codeHtml = `
`; // Push the current code block's HTML to the array and replace with a placeholder codeBlocks.push(codeHtml); return `CODEBLOCK${codeBlocks.length - 1}\n`; }); markdown = markdown.replace(/```/g, '`'); // markdown = markdown.replace(/(?:\n\n|^)```(\w+)?\s*([\s\S]*?)\n\n```(?:\n\n|$)/gm, function (match, pre, lang, code, post, index) { // lang = lang || 'plaintext'; // const escapedCode = code.replace(//g, '>'); // const codeHtml = `
`; // // Push the current code block's HTML to the array and replace with a placeholder // codeBlocks.push(codeHtml); // return `CODEBLOCK${codeBlocks.length - 1}`; // }); // 2. Convert headers markdown = markdown.replace(/^#####\s+(.+)$/gm, '${lang}${escapedCode}
' + code.replace(//g, '>') + '
';
// });
// 对于内联代码的匹配,我们使用否定的前瞻和后顾来确保反引号前后没有其他反引号
markdown = markdown.replace(/(?' + code.replace(//g, '>') + '';
});
// 7. Convert bold text
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '$1');
// 8. Convert paragraphs
markdown = markdown.replace(/^(?!|
||<\/?code>)([\s\S]+?)(?=\n\n|$)/gm, '$1
'); markdown = markdown.replace(/CODEBLOCK(\d+)/g, function (match, n) { return codeBlocks[n]; }); return markdown; } // function markdownToHTML(markdown) { // // Convert bold text // markdown = markdown.replace(/\*\*(.*?)\*\*/g, '$1'); // // markdown = markdown.replace(/```(\w+)?\s*([\s\S]*?)```/g, function (match, lang, code) { // // 如果没有指定语言,我们可以默认使用'plaintext',或者选择不显示 // lang = lang || 'plaintext'; // return ``; // }); // // // Convert inline code and escape HTML // markdown = markdown.replace(/`([^`]+)`/g, function (match, code) { // return '${lang}${code.trim()} //
' + code.replace(//g, '>') + '
'; // }); // // // Convert ordered lists // markdown = markdown.replace(/\d+\.\s+(.+)(\n|$)/g, '$1 '); // markdown = '' + markdown + '
'; // // // Convert unordered lists (assuming they are marked with - ) // markdown = markdown.replace(/^-\s+(.+)(\n|$)/gm, '$1 '); // markdown = '' + markdown + '
'; // // // Convert paragraphs // markdown = markdown.replace(/([^\n]+)(\n|$)/g, '$1
'); // // return markdown; // } // Convert code blocks // markdown = markdown.replace(/```([\s\S]*?)```/g, function (match, code) { // return ``; // }); // // Convert code blocks with language detection // markdown = markdown.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, function(match, language, code) { // return `javascript${code.trim()} //
`; // }); // // if(d.data.content[i].content_type && d.data.content[i].content_type === "image_asset_pointer"){ // let allPicsDiv = createAllPicsDiv(); // allPicsDiv.id = "allPicsDiv"; // realContentDiv.appendChild(allPicsDiv); // while(d.data.content[i] && d.data.content[i].content_type === "image_asset_pointer"){ // // const regex = /file-[\w-]+/; // // const matchedstring = d.data.content[i].asset_pointer.match(regex); // const matchedstring = d.data.content[i].asset_pointer.slice(15); // 从第16个字符开始截取到字符串结尾 // // if (matchedstring) { // log(matchedstring); // file-0rbJDxE2JZqAUM8R9Jz8eUOS // let sourceURL = pictureURL.get(matchedstring) ; // if (!sourceURL) { // try { // sourceURL = await fetchPictureURL(matchedstring); // pictureURL.set(matchedstring, sourceURL); // } catch (error) { // console.error(error); // return; // 如果有错误,提前退出函数 // } // } // // let picDiv = createOnePic(sourceURL); // let downloadBtn = createDownloadBtn(); // downloadBtn.dataset.sourceUrl = sourceURL; // 在所有情况下都设置 // allPicsDiv.appendChild(picDiv); // allPicsDiv.appendChild(downloadBtn); // } else { // log("No match found"); // } // i++; // } // if ( i < d.data.content.length) { // let contentSpan = document.createElement('div'); // contentSpan.className = 'content-text'; // contentSpan.innerHTML = d.data.content[i]; // 如果只需要纯文本 // realContentDiv.appendChild(contentSpan); // } // } // if(d.data.content_type === "text"){ // let div = document.createElement('div'); // div.innerHTML = d.data.content[0]; // div.className = 'content-text'; // realContentDiv.appendChild(div); // } // else { // for(let i = 0 ; i < d.data.content.length; i ++){ // if(d.data.content[i].content_type && d.data.content[i].content_type === "image_asset_pointer"){ // let allPicsDiv = createAllPicsDiv(); // allPicsDiv.id = "allPicsDiv"; // realContentDiv.appendChild(allPicsDiv); // while(d.data.content[i] && d.data.content[i].content_type === "image_asset_pointer"){ // // const regex = /file-[\w-]+/; // // const matchedstring = d.data.content[i].asset_pointer.match(regex); // const matchedstring = d.data.content[i].asset_pointer.slice(15); // 从第16个字符开始截取到字符串结尾 // // if (matchedstring) { // log(matchedstring); // file-0rbJDxE2JZqAUM8R9Jz8eUOS // let sourceURL = pictureURL.get(matchedstring) ; // if (!sourceURL) { // try { // sourceURL = await fetchPictureURL(matchedstring); // pictureURL.set(matchedstring, sourceURL); // } catch (error) { // console.error(error); // return; // 如果有错误,提前退出函数 // } // } // // let picDiv = createOnePic(sourceURL); // let downloadBtn = createDownloadBtn(); // downloadBtn.dataset.sourceUrl = sourceURL; // 在所有情况下都设置 // allPicsDiv.appendChild(picDiv); // allPicsDiv.appendChild(downloadBtn); // } else { // log("No match found"); // } // i++; // } // if ( i < d.data.content.length) { // let contentSpan = document.createElement('div'); // contentSpan.className = 'content-text'; // contentSpan.innerHTML = d.data.content[i]; // 如果只需要纯文本 // realContentDiv.appendChild(contentSpan); // } // } // else{ // let contentSpan = document.createElement('div'); // contentSpan.className = 'content-text'; // contentSpan.innerHTML = d.data.content[i]; // 如果只需要纯文本 // realContentDiv.appendChild(contentSpan); // } // } // } targetDiv.appendChild(realContentDiv); document.querySelectorAll('div.chattreecodeblock code').forEach(el => { // then highlight each hljs.highlightElement(el); }); }, handleClick: function (e) { if (e.target.closest('button.flex.ml-auto.gap-2.items-center')) { var codeBlock = e.target.closest('pre')?.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("右击了节点!"); analyzeKit.uploadActivityLog(5) 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 = { log(category, ...message) { // 使用 Logger 实例的 log 方法 //console.log("moduleLog called."); this.logger.log(category, ...message); }, initTemporaryWindowKit: function () { this.logger = new Logger('temporaryWindowKit'); //this.log(LogCategories.SUCCESS, 'temporaryWindowKitLoggerCreated!'); }, 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; width: 600px; padding: 20px; font-size: 16px; lineHeight: 1.6; border: 1px solid #ccc; borderRadius: 5px; boxShadow: 0px 0px 10px rgba(0,0,0,0.2); overflow: auto; opacity: 0.8; max-Height: 500px;`; //fontFamily: Arial, sans-serif; nodesInAndOutKit.updateSelectedNodeContentDiv(d, windowElem); const dark = isDark(); windowElem.classList.add('markdown', 'prose', 'w-full', 'break-words', 'dark:prose-invert', dark ? "dark" : "light"); windowElem.style.backgroundColor = 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 = 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); }, } temporaryWindowKit.initTemporaryWindowKit(); //---ContentKit---// const contentDiv = document.createElement('div'); const selectedNodeContent = document.createElement('div'); const contentHeader = document.createElement('div'); let goToNodeButton;//注意gotoNode的逻辑! const talkingPerson = document.createElement('div'); const commentForm = document.createElement('div'); const commentLabel = document.createElement('label'); const commentTextarea = document.createElement('textarea'); let submitButton = document.createElement('button'); let cancelButton = document.createElement('button'); let clearButton = document.createElement('button'); function updateStylesBasedOnTheme() { const dark = isDark() contentDiv.style.backgroundColor = dark ? 'rgb(68,70,84)' : 'rgb(256,256,256)'; contentDiv.style.boxShadow = 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 (dark) { links .attr("stroke", "white") } else { links .attr("stroke", "black") } commentTextarea.style.background = 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'; contentHeader.id = 'contentHeader'; 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.style = `position :fixed; top: 10px; right: 10px; width: 400px; padding: 10px; border :8px solid #ddd; display :none; lineHeight :1.6; overflow :hidden; userSelect: text; font-size: 14px;`; contentDiv.id = 'contentDiv'; contentDiv.style.borderRadius = '8px'; 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'); let copyButton = ButtonCreator.createButton({ id: 'copyButton', innerHTML: '📋', eventListeners: [ { type: 'click', handler: () => { ContentKit.copyToClipboard(selectedNodeContent.innerHTML.replace(/<[^>]*>/g, ' ')); ButtonOperations.showUserNotification(translate("contentCopied")); } }, ], additionalStyles: { border: 'none', cursor: 'pointer', fontSize: '20px', position: 'absolute', top: '6px', right: '150px', background: 'none', }, }); goToNodeButton = ButtonCreator.createButton({ id: 'goToNodeButton', text: '🚩', eventListeners: [ {type: 'click', handler: () => DOMOperations.jumpToDialogueItem(contentDiv.dataset.curDisplay)}, ], additionalStyles: { border: 'none', cursor: 'pointer', fontSize: '18px', position: 'absolute', top: '7px', right: '90px', background: 'none', }, }); let commentButton = ButtonCreator.createButton({ id: 'commentButton', text: '🖊', eventListeners: [ {type: 'click', handler: this.commentToSave}, ], additionalStyles: { border: 'none', cursor: 'pointer', fontSize: '18px', position: 'absolute', top: '8px', right: '210px', background: 'none', }, }); talkingPerson.id = 'talkingPerson'; talkingPerson.style.position = 'absolute'; talkingPerson.style.top = '8px'; talkingPerson.style.right = '240px'; talkingPerson.style.background = 'none'; let closeButton = ButtonCreator.createButton({ id: 'closeButton', innerHTML: '', eventListeners: [ { type: 'click', handler: () => { contentDiv.style.display = 'none'; if (commentForm.style.display === 'block') commentForm.style.display = 'none'; } } ], additionalStyles: { border: 'none', cursor: 'pointer', fontSize: '30px', position: 'absolute', top: '10px', right: '40px', backgroundColor: 'white', }, }); commentForm.id = 'commentForm'; commentForm.style.position = 'fixed'; ContentKit.positionCommentFormRelativeToContentDiv(); commentForm.style.display = 'none'; commentLabel.setAttribute('for', 'comment'); commentLabel.innerText = translate("enterComment") + ":"; commentTextarea.id = 'commentText'; commentTextarea.rows = '5'; cancelButton.className = 'commentHoverEffect'; clearButton.className = 'commentHoverEffect'; submitButton.id = 'submitComment'; submitButton.innerText = translate("userCommentSave"); submitButton.className = 'commentHoverEffect'; commentForm.appendChild(commentLabel); commentForm.appendChild(commentTextarea); 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(); } } 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'; } } //log("contentDiv:",contentDiv); let imagesBtns = contentDiv.querySelectorAll('.m-1'); //log("images:",images); let contentWidth = contentDiv.offsetWidth; imagesBtns.forEach(btn => { btn.style.width = `${contentWidth / 2 - 60}px`; btn.style.height = `${contentWidth / 2 - 60}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) { analyzeKit.uploadActivityLog(4) let curUUID = contentDiv.dataset.curDisplay; log("curUUID:", curUUID); if (commentForm.style.display !== 'block') { commentForm.style.display = 'block'; } else { commentForm.style.display = 'none'; return; } 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 = ''; conversationData.commentMap.delete(selectedMapNode.uuid); log("renew selectedNodeData:", selectedNodeData); //log("selectedNodeData.comment:", selectedNodeData.data.comment); selectedMapNode.comment = ''; const existingComment = ""; 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'; nodesInAndOutKit.updateSelectedNodeContentDiv(selectedNodeData, selectedNodeContent); } else { ButtonOperations.showUserNotification(translate("enterCommentFirst"), "red"); } } else { ButtonOperations.showUserNotification(translate("commentSaved")); try { selectedNodeData.data.comment = commentValue; conversationData.commentMap.set(selectedNodeData.data.uuid, commentValue); log("conversationData:", conversationData); 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___// //---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'); let languageSelectDiv = document.createElement('div'); let WeChatDiv = document.createElement('div'); let TencentDiv = document.createElement('div'); const WeChatPicDiv = document.createElement('div'); const TencentPicDiv = document.createElement('div'); const feedBackPicDiv = document.createElement('div'); let rightMiddleMenu = document.createElement("div"); let toggleMainBtnMovingAccessbility = document.createElement("div"); let languageContainer = document.createElement('div'); let languageSelect = document.createElement('select'); let scaleIncrementSmall = 0.1; let scaleIncrementLarge = 0.3; let centerX = window.innerWidth / 2; let centerY = window.innerHeight / 3; let undoStack = []; let redoStack = []; let newOperation = {}; let redForEnterable = ` `; let greenForNotEnter = ``; 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); languageSelectDiv.id = "languageSelectDiv"; languageSelectDiv.className = "rightMiddleDiv"; languageSelectDiv.innerText = globalUserLang; languageSelectDiv.style.fontSize = '12px'; rightMiddleContainer.appendChild(languageSelectDiv); WeChatDiv.id = "WeChatDiv"; WeChatDiv.className = "rightMiddleDiv"; WeChatDiv.innerHTML = ` ` ; rightMiddleContainer.appendChild(WeChatDiv); TencentDiv.id = "TencentDiv"; TencentDiv.className = "rightMiddleDiv"; TencentDiv.innerHTML = ` `; rightMiddleContainer.appendChild(TencentDiv); languageContainer.className = "language-container"; languageContainer.style.position = 'fixed'; languageContainer.style.display = 'none'; languageSelect.id = 'languageSelect'; languageSelect.className = "language-dropdown"; let languages = ['ar', 'bg', 'ckb', 'cs', 'da', 'de', 'el', 'en', 'eo', 'es', 'fi', 'fr', 'fr-CA' , 'he', 'hu', 'id', 'it', 'ja', 'ka', 'ko', 'nb', 'nl', 'pl', 'pt-PT', 'pt-BR', 'ro', 'ru', 'sk' , 'sr', 'sv', 'th', 'tr', 'uk', 'ug', 'vi', 'zh-CN', 'zh-TW', 'zh-HK', 'zh-SG'] languages.forEach(lang => { let option = document.createElement('option'); option.value = lang; option.innerText = lang; if (lang === globalUserLang) { option.selected = true; } languageSelect.appendChild(option); }); languageContainer.appendChild(languageSelect); rightMiddleContainer.appendChild(languageContainer); toggleMainBtnMovingAccessbility.id = "toggleMainBtnMovingAccessbility"; toggleMainBtnMovingAccessbility.className = "rightAlwaysShownDiv"; toggleMainBtnMovingAccessbility.addEventListener('click', ButtonOperations.toggleStickyMainBtn) document.body.appendChild(toggleMainBtnMovingAccessbility); feedBackPicDiv.style.display = 'none'; rightMiddleContainer.appendChild(feedBackPicDiv); WeChatPicDiv.style.display = 'none'; rightMiddleContainer.appendChild(WeChatPicDiv); TencentPicDiv.style.display = 'none'; rightMiddleContainer.appendChild(TencentPicDiv); document.body.appendChild(settingsContainer); document.body.appendChild(rightMiddleContainer); }, deletechatgptDB: function () { const DB_NAME = 'ChatTreeDB'; console.log('try to delete:'); var req = indexedDB.deleteDatabase(DB_NAME); req.onsuccess = function () { console.log("Deleted database successfully"); }; req.onerror = function () { console.log("Couldn't delete database"); }; req.onblocked = function () { console.log("Couldn't delete database due to the operation being blocked"); }; }, toggleLanguageSelectShow: function () { if (languageContainer.style.display !== 'none') { languageContainer.style.display = 'none'; return; } languageContainer.style.display = 'flex'; analyzeKit.uploadActivityLog(9) document.addEventListener('click', (e) => { if (!rightMiddleContainer.contains(e.target)) { languageContainer.style.display = 'none'; } }); }, toggleColorSelectShow: function () { if (rightMiddleMenu.style.display === 'flex') { rightMiddleMenu.style.display = 'none'; return; } analyzeKit.uploadActivityLog(7) 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 SVGOpacityPicker = document.getElementById('SVGOpacityPicker'); let SVGColorTopPicker = document.getElementById('SVGColorTopPicker'); let SVGColorBottomPicker = document.getElementById('SVGColorBottomPicker'); let chatGPTColorTopPicker = document.getElementById('chatGPTColorTopPicker'); let chatGPTColorBottomPicker = document.getElementById('chatGPTColorBottomPicker'); let userColorTopPicker = document.getElementById('userColorTopPicker'); let userColorBottomPicker = document.getElementById('userColorBottomPicker'); //改变 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); chatGPTColorTopPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onChatGPTColorTopPickerChangeDone); chatGPTColorBottomPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onChatGPTColorBottomPickerChangeDone); userColorTopPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onUserColorTopPickerChangeDone); userColorBottomPicker.addEventListener('change', settingsKit.colorAndOpacityKit.onUserColorBottomPickerChangeDone); 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)); SVGOpacityPicker.value = opacity; SVGColorTopPicker.value = rgbToHex(topColor); SVGColorBottomPicker.value = rgbToHex(bottomColor); chatGPTColorTopPicker.value = states.colorSetting.customChatGPT.top; chatGPTColorBottomPicker.value = states.colorSetting.customChatGPT.bottom; userColorTopPicker.value = states.colorSetting.customUser.top; userColorBottomPicker.value = states.colorSetting.customUser.bottom; } }, 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); }); }, onChatGPTColorTopPickerChangeDone: function (e) { log("ChangeDone!"); states.colorSetting.customChatGPT.is = true; // 移除现有的渐变 // const existingGradient = defs.querySelector('#CUSTOM_USER_GRADIENT'); const defsNode = defs.node(); const existingGradient = defsNode.querySelector('#CUSTOM_CHATGPT_GRADIENT'); if (existingGradient) { defsNode.removeChild(existingGradient); } states.colorSetting.customChatGPT.top = e.target.value.toString(); initSvgAndGradient.createLinearGradient(defs, "CUSTOM_CHATGPT_GRADIENT", states.colorSetting.customChatGPT.bottom, states.colorSetting.customChatGPT.top); drawMainSVG(); const newSettings = {id: 'chatGPTColor', state: states.colorSetting.customChatGPT}; log("newGPTSetting:", newSettings); dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onChatGPTColorBottomPickerChangeDone: function (e) { log("ChangeDone!"); states.colorSetting.customChatGPT.is = true; // 移除现有的渐变 const defsNode = defs.node(); const existingGradient = defsNode.querySelector('#CUSTOM_CHATGPT_GRADIENT'); if (existingGradient) { defsNode.removeChild(existingGradient); } states.colorSetting.customChatGPT.bottom = e.target.value.toString(); initSvgAndGradient.createLinearGradient(defs, "CUSTOM_CHATGPT_GRADIENT", states.colorSetting.customChatGPT.bottom, states.colorSetting.customChatGPT.top); drawMainSVG(); const newSettings = {id: 'chatGPTColor', state: states.colorSetting.customChatGPT}; log("newGPTSetting:", newSettings); dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onUserColorTopPickerChangeDone: function (e) { log("ChangeDone!"); states.colorSetting.customUser.is = true; // 移除现有的渐变 const defsNode = defs.node(); const existingGradient = defsNode.querySelector('#CUSTOM_USER_GRADIENT'); if (existingGradient) { defsNode.removeChild(existingGradient); } states.colorSetting.customUser.top = e.target.value.toString(); initSvgAndGradient.createLinearGradient(defs, "CUSTOM_USER_GRADIENT", states.colorSetting.customUser.bottom, states.colorSetting.customUser.top); drawMainSVG(); const newSettings = {id: 'userColor', state: states.colorSetting.customUser}; log("newUserSetting:", newSettings); dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); }, onUserColorBottomPickerChangeDone: function (e) { log("ChangeDone!"); states.colorSetting.customUser.is = true; // 移除现有的渐变 const defsNode = defs.node(); const existingGradient = defsNode.querySelector('#CUSTOM_USER_GRADIENT'); if (existingGradient) { defsNode.removeChild(existingGradient); } states.colorSetting.customUser.bottom = e.target.value.toString(); initSvgAndGradient.createLinearGradient(defs, "CUSTOM_USER_GRADIENT", states.colorSetting.customUser.bottom, states.colorSetting.customUser.top); drawMainSVG(); const newSettings = {id: 'userColor', state: states.colorSetting.customUser}; log("newUserSetting:", newSettings); dbOperations.updateUserSettings(newSettings).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); } }, addEventListeners: function () { languageSelectDiv.addEventListener('click', settingsKit.toggleLanguageSelectShow); 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'); } }); // 全局保存所有的PicDivs let allPicDivs = []; function handleMouseEvents(mainDiv, picDiv) { let disappearTimeOut; // 当一个PicDiv被展示时,隐藏所有其他的PicDivs function hideOtherPicDivs(currentPicDiv) { for (let div of allPicDivs) { if (div !== currentPicDiv) { div.style.display = 'none'; } } } mainDiv.addEventListener('mouseenter', function (e) { log(mainDiv.id + " mouseover"); clearTimeout(disappearTimeOut); hideOtherPicDivs(picDiv); // 隐藏其他PicDivs let elements = [mainDiv, picDiv]; elements.forEach(element => { element.style.display = 'flex'; }); }); picDiv.addEventListener('mouseenter', function (e) { log(picDiv.id + " mouseover"); clearTimeout(disappearTimeOut); hideOtherPicDivs(picDiv); // 隐藏其他PicDivs picDiv.style.display = 'block'; }); mainDiv.addEventListener('mouseleave', function () { log(mainDiv.id + " mouseleave"); disappearTimeOut = setTimeout(() => { picDiv.style.display = 'none'; }, 400); }); picDiv.addEventListener('mouseleave', function (e) { log(picDiv.id + " mouseleave"); disappearTimeOut = setTimeout(() => { picDiv.style.display = 'none'; }, 400); }); // 将当前的PicDiv加入到集中管理的数组中 allPicDivs.push(picDiv); } handleMouseEvents(feedbackDiv, feedBackPicDiv); handleMouseEvents(WeChatDiv, WeChatPicDiv); handleMouseEvents(TencentDiv, TencentPicDiv); // let feedbackTimeOut; // feedbackDiv.addEventListener('mouseenter', function (e) { // log("feedbackDiv mouseover"); // clearTimeout(feedbackTimeOut); // let elements = [feedbackDiv, feedBackPicDiv]; // elements.forEach(element => { // element.style.display = 'flex'; // }); // }); // feedBackPicDiv.addEventListener('mouseenter', function (e) { // log("feedBackPicDiv mouseover"); // clearTimeout(feedbackTimeOut); // feedBackPicDiv.style.display = 'block'; // }); // feedbackDiv.addEventListener('mouseleave', function () { // log("feedbackDiv mouseleave"); // feedbackTimeOut = setTimeout(() => { // feedBackPicDiv.style.display = 'none'; // }, 400); // }); // feedBackPicDiv.addEventListener('mouseleave', function (e) { // log("feedBackPicDiv mouseleave"); // feedbackTimeOut = setTimeout(() => { // feedBackPicDiv.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"), 'red'); if (IsfromPanel) { return; } try { dbOperations.loadConversationsData(operatingURL).then(loadeddata => { log("Loaded data for URL:", loadeddata); conversationData = loadeddata; root = d3.hierarchy(conversationData.rootNode); treeLayout(root); settingsKit.refreshTree(); }).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; }); }, 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); }, }; 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.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 () { analyzeKit.uploadActivityLog(8) 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 (current.uuid === conversationRootNode.uuid) { queue.push(...current.children); continue; } //log("In BFSSearch, current_content:",current.content); if (searchOption === translate("searchInContent") && current.content) { log("搜索内容"); if(!current.content.length){ if (!current.content.match(new RegExp(searchTerm, 'i'))){ results.push(current); continue; } } for (let i = 0; i < current.content.length; i++) { if(!current.content[i].parts) continue; for (let j = 0; j < current.content[i].parts.length; j++) { if (!current.content[i].parts[j].content_type && current.content[i].parts[j].match(new RegExp(searchTerm, 'i'))) results.push(current); } } } else if (searchOption === translate("searchInComments")) { log("搜索评论"); if (conversationData.commentMap.get(current.uuid)) results.push(current); } else if (searchOption === translate("searchInBoth") && (current.content)) { log("搜索两者"); if(!current.content.length){ if (!current.content.match(new RegExp(searchTerm, 'i'))){ results.push(current); } if (current.children) { queue.push(...current.children); } continue; } for (let i = 0; i < current.content.length; i++) { if(!current.content[i].parts) continue; for (let j = 0; j < current.content[i].parts.length; j++) { if (!current.content[i].parts[j].content_type && current.content[i].parts[j].match(new RegExp(searchTerm, 'i'))) results.push(current); } } if (conversationData.commentMap.get(current.uuid)) 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); }); }, }; 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 panelHeader = document.createElement('div'); let filteredConversations = []; const controlPanelKit = { init: function () { panelToggleButton = ButtonCreator.createButton({ id: 'panelToggleButtonSVGShow', text: translate("openAdminPanel"), eventListeners: [ {type: 'click', handler: controlPanelKit.toggleHistoryPanel}, ], additionalStyles: { display: 'none', borderRadius: '12px', opacity: '0.9', background: 'linear-gradient(to right, #007BFF, #00C6FF)', color: 'white', padding: '10px 20px', fontWeight: 'bold', 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); panelHeader.id = 'panelHeader'; let translatedTitle = translate('conversationTitle'); let translatedOption = translate('actionOptions'); let translatedCategory = translate('conversationCategory'); let translatedTags = translate('conversationTags'); panelHeader.innerHTML = `${language || 'plain text'}${code.trim()} //
${translatedTitle}${translatedCategory}
`; // 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 () { 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); }); }, }; const languageKits = { init: function (e) { log("changeLanguageclick!"); console.log("globalUserLang: ",globalUserLang); languageKits.updateUIWithTranslations(); if (!db) { return; } document.getElementById('search-results-count').innerText = ''; if (e) { log("e.target.value:", e.target.value); globalUserLang = e.target.value; currentLangPack = getLang(globalUserLang).langPack; log("currentLangPack:", currentLangPack); languageSelectDiv.innerText = e.target.value; // languageContainer.style.display = 'none'; languageKits.updateUIWithTranslations(); if (e.target.shouldNotSave) { return; } const newSetting = {id: "userLang", globalUserLang: globalUserLang}; dbOperations.updateUserSettings(newSetting).then(() => { ButtonOperations.showUserNotification(translate("successSavingChanges")); }).catch(error => { console.error("Error saving Change:", error); }); } }, updateUIWithTranslations: function () { // 获取optionsButtons元素 const optionsButtonsDiv = document.getElementById("optionsButtons"); controlPanelKit.updateCategorySelect(); if (!optionsButtonsDiv) { console.error("optionsButtonsDiv not found"); return; } // 更新单选按钮标签 const radioLabels = optionsButtonsDiv.querySelectorAll("label"); const optionsTranslated = [translate("searchInContent"), translate("searchInComments"), translate("searchInBoth")]; radioLabels.forEach((label, index) => { if (optionsTranslated[index]) { log("Updating label:", label); // 打印所有子节点以找到正确的文本节点索引 label.childNodes.forEach((child, idx) => { log("Child node at index", idx, ":", child); }); // 获取当前label中的文本节点并更新 const textNode = label.lastChild; // 也许我们可以直接获取最后一个子节点作为文本节点 if (textNode && textNode.nodeType === Node.TEXT_NODE) { textNode.nodeValue = optionsTranslated[index]; } else { console.error("No text node found for label", label); } } }); const navButtonsElems = optionsButtonsDiv.querySelectorAll("button"); const navButtonsTranslated = [translate("goToPrevious"), translate("goToNext")]; navButtonsElems.forEach((button, index) => { if (navButtonsTranslated[index]) { button.innerText = navButtonsTranslated[index]; } }); navPanelButton.textContent = translate("openAdminPanel"); //contentHeader.textContent = translate("nodeDetails"); commentLabel.innerText = translate("enterComment") + ":"; cancelButton.innerText = translate("userCommentCancel"); clearButton.innerText = translate("userCommentClear"); searchBtn.innerText = translate("searchButton"); panelToggleButton.innerText = translate("openAdminPanel"); searchTopicBox.setAttribute("placeholder", translate("searchPlaceholder")); searchBox.setAttribute("placeholder", translate("searchPlaceholder")); { let translatedTitle = translate('conversationTitle'); let translatedOption = translate('actionOptions'); let translatedCategory = translate('conversationCategory'); let translatedTags = translate('conversationTags'); panelHeader.innerHTML = `
${translatedTitle}${translatedCategory}
`; } if (globalUserLang.startsWith('zh')) { feedBackPicDiv.innerHTML = ``; } else { feedBackPicDiv.innerHTML = `点击参与问卷调查 (腾讯问卷)
`; } WeChatPicDiv.innerHTML = `Click to take the survey(Google Forms)
` TencentPicDiv.innerHTML = `作者微信
` } } function fetchRawChatDetails() { return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token => { log("get Token:",token); const xhr = new XMLHttpRequest(); xhr.open('GET', endpoints.openAI.chats, true); // `endpoints` object should be accessible from the original script xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.onload = () => { if (xhr.status !== 200) { reject('Request failed. Cannot retrieve chat details.'); } else { resolve(JSON.parse(xhr.responseText)); } }; xhr.onerror = () => { reject('Request error.'); }; xhr.send(); }); }); } function fetchRawChatMessages(chatId = "") { if (chatId === "") { return; } return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token => { //token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1UaEVOVUpHTkVNMVFURTFgoLYTsrmDFyiz9d6coNcS4YLMpkufWtaNRU13UmtGRVFrRXpSZyJ9.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiJwdGN5ZGRkbjl6QHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InBvaWQiOiJvcmctNlZoeUlReGlGQWlPbW1hVXg5WHlETFgoLYTsrmDFyiz9d6coNcS4YLMpkufWtaFNZkU5emhHcndUdEJ1SGFheGszIn0sImlzcyI6Imh0dHBzOi8vYXV0aDAub3BlbmFpLmNvbS8iLCJzdWIiOiJhcHBsZXwwMDAxMjMuNjQ4NjIzMDE5NmZlNDhiM2I4MTFgoLYTsrmDFyiz9d6coNcS4YLMpkufWta6WyJodHRwczovL2FwaS5vcGVuYWkuY29tL3YxIiwiaHR0cHM6Ly9vcGVuYWkub3BlbmFpLmF1dGgwYXBwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE2OTc4ODYyNTFgoLYTsrmDFyiz9d6coNcS4YLMpkufWtaiVGRKSWNiZTFgoLYTsrmDFyiz9d6coNcS4YLMpkufWtaiLCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIG1vZGVsLnJlYWQgbW9kZWwucmVxdWVzdCBvcmdhbml6YXRpb24ucmVhZCBvcmdhbml6YXRpb24ud3JpdGUgb2ZmbGluZV9hY2Nlc3MifQ.KDeLRadnLd9vzMzbrhJC7u65mwQHY0E7Hd3wOTzJpnAN0qLvzKTGA_tDdmglcW4qIJcZddTFU2Hn7YJt1DC3MSXfZNdC1sqKk0Uj9ep-iodiNCmCo9O1V-9JTh0GcW75BmbSOe5L4hAguJYhAhz2xaGs1zfr6gFBXrqdNxjzKiN-mrKtm4hjkWTdWdf-KZC2ZPun81h2k30x2hsBDxIJIwv8PeAYIZKzQJJdAA9V6X1WXQZX1vI79rp3tnIk-WAJgtW-U1F_UgR8bKRivfgUYSa2NCpCPimnU_LDLvipE8jZMrSTxU8amdb_0Z22YiUvTL-wFWVg6m5IKg-82x-zBQ"; const xhr = new XMLHttpRequest(); xhr.open('GET', `${endpoints.openAI.chat}/${chatId}`, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.onload = () => { if (xhr.status !== 200) { reject('Request failed. Cannot retrieve chat messages.'); } else { resolve(JSON.parse(xhr.responseText)); // 返回整个原始数据 } }; xhr.onerror = () => { reject('Request error.'); }; xhr.send(); }); }); } function fetchAccountDetails() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', endpoints.openAI.session, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { let getResponse = JSON.parse(xhr.responseText); return resolve(getResponse); }; xhr.send(); }); } function fetchAccountMe() { return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token=>{ const xhr = new XMLHttpRequest(); xhr.open('GET', endpoints.openAI.me, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.onload = () => { let getResponse = JSON.parse(xhr.responseText); return resolve(getResponse); }; xhr.send(); }) }); } let pictureURL = new Map(); function fetchPictureURL(asset_pointer = "file-P2IMiUABxJbug9oU8Vh3sBBZ") { return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token => { const xhr = new XMLHttpRequest(); let requestURL = `${domain}/backend-api/files/` + asset_pointer + "/download"; log("requestURL:", requestURL); xhr.open('GET', requestURL, true); xhr.setRequestHeader('Content-Type', 'application/json'); // 可以考虑删除此行 xhr.setRequestHeader('Authorization', 'Bearer ' + token); log("XHR sent!"); xhr.onload = () => { if (xhr.status !== 200) { reject('Request failed. Cannot retrieve the file.'); } else { let getResponse = JSON.parse(xhr.responseText); resolve(getResponse.download_url); // 直接返回响应文本或数据 } }; xhr.onerror = () => { reject('Request error.'); }; xhr.send(); }); }); } let fileHeader = `${domain}/backend-api/files`; fetchPictureURL().then(data => { log("fetchURL: ", data); // 打印原始聊天数据 }).catch(error => { console.error(error); }); let conversationRootNode; /** * 主处理函数,用于处理聊天消息。 * @param {Object} data - 包含消息映射和其他聊天相关数据的对象。 */ function processChatMessage(data) { log("In ProcessChatMessage, mapping:", data.mapping); const tokenMap = new Map(Object.entries(data.mapping)); let accessToken = chatgpt.getAccessToken(); log(tokenMap); // 显示映射对象,以便于调试 fetchAccountDetails().then(userData => { fetchAccountMe().then(medata=>{ console.log("medata:",medata) log("user DATA:", userData); conversationData.participants.user.name = userData.user.name; conversationData.participants.user.avatarURL = medata.picture; }) }); conversationData.url = `${domain}/c/` + data.conversation_id; let gptInfoDiv = document.querySelector('.flex.flex-1.flex-grow.items-center.gap-1.px-2.py-1.text-gray-600'); log("GPTINFOR:", gptInfoDiv); if (gptInfoDiv) { conversationData.participants.gpt.type = gptInfoDiv.innerText; } log("conversationData.participants.gpt.type:", conversationData.participants.gpt.type); let uuid2nodeMap = new Map(); let uuid2pathMap = new Map(); for (let [key, value] of tokenMap) { if (!value.parent) { let tokenRootNode = tokenMap.get(value.children[0]); conversationData.title = data.title; if (tokenRootNode) { log("rootNode:", tokenRootNode); conversationRootNode = new DialogueNode(data.title ? data.title : Default_RootNode_Content, "chatGPT", tokenRootNode.id, -1); uuid2nodeMap.set(tokenRootNode.id, conversationRootNode) } else { log("Creating RootNode Wrong! Exit Updating Map!"); return; } /** * 深度优先搜索,过滤出第一个子节点,特别关注用户节点。 * @param {string} currentRootNodeId - 当前根节点的 ID。 */ function DFSFilterFirstChildren(currentRootNodeId) { let currentRootNode = tokenMap.get(currentRootNodeId); console.log("currentRootNode: ", currentRootNode); currentRootNode.children.slice().forEach(childId => { let child = tokenMap.get(childId); if (child && child.message && child.message.author && child.message.author.role.toLowerCase() === "user") { if (!tokenRootNode.children.includes(childId)) tokenRootNode.children.push(childId); return; } if (child && child.children) { child.children.forEach(grandsonId => { let grandson = tokenMap.get(grandsonId); if (grandson && grandson.message && grandson.message.author && grandson.message.author.role.toLowerCase() === "user") { if (!tokenRootNode.children.includes(grandsonId)) tokenRootNode.children.push(grandsonId); return; } DFSFilterFirstChildren(grandsonId); }); } let index = tokenRootNode.children.indexOf(childId); if (index > -1) { tokenRootNode.children.splice(index, 1); } }); } /** * 深度优先搜索更新映射,用于构建对话节点。 * @param {Object} tokenNode - 当前处理的token节点。 * @param {Object} curRootNode - 当前的根节点对象。 * @param {number} parentID - 父节点的ID。 */ function DFSUpdateMap(tokenNode, curRootNode, parentID) { for (let i = 0; i < tokenNode.children.length; i++) { let child = tokenMap.get(tokenNode.children[i]); let a_node_content = []; let nodeID = child.id; let parent = child.parent; if (child.message.author.role.toLowerCase() !== "user") { updateChildContent(child); a_node_content.push(child.message.content); let grandSon; while (child.message.author.role.toLowerCase() !== "user" && child.children.length > 0) { grandSon = tokenMap.get(child.children[0]); if (grandSon.message.author.role.toLowerCase() === "user") break; updateChildContent(grandSon); a_node_content.push(grandSon.message.content); child = grandSon; } if (child.message.author.role.toLowerCase() === "user") { child = tokenMap.get(child.parent); } } else { updateChildContent(child); a_node_content.push(child.message.content); } let type = child.message.author.role.toLowerCase() === "user" ? "用户" : "chatGPT"; let newDialogueNode = new DialogueNode(a_node_content, type, nodeID, parentID); curRootNode.children.push(newDialogueNode); uuid2nodeMap.set(child.id, newDialogueNode); if (child.children.length > 0) { DFSUpdateMap(child, newDialogueNode, nodeID); } } } /** * 更新子节点的内容。 * @param {Object} child - 子节点对象。 */ function updateChildContent(child) { child.message.content.author = child.message.author; child.message.content.create_time = child.message.create_time; child.message.content.metadata = child.message.metadata; child.message.content.recipient = child.message.recipient; child.message.content.status = child.message.status; child.message.content.weight = child.message.weight; } DFSFilterFirstChildren(tokenRootNode.id); DFSUpdateMap(tokenRootNode, conversationRootNode, tokenRootNode.id); uuid2pathMap.set(conversationRootNode.uuid, ""); DFSUpdatePathMap(conversationRootNode, [], uuid2pathMap); log("uuid2nodeMap:", uuid2nodeMap); log("uuid2pathMap:", uuid2pathMap); conversationData.rootNode = conversationRootNode; conversationData.uuid2nodeMap = uuid2nodeMap; conversationData.uuid2pathMap = uuid2pathMap; root = d3.hierarchy(conversationRootNode); treeLayout(root); settingsKit.refreshTree(); handleTreeChange() setTimeout(async ()=>{ const data = await getAllTrees(); multiVersionTrees.generateVersionOptions(data.names.map(i=>({name: i, value: i, selected: i === defaultTreeName}))) }, 1000) break; } } return { uuid2nodeMap: uuid2nodeMap, uuid2pathMap: uuid2pathMap }; } function getAllTrees(){ let key = conversationData.rootNode.uuid + CONVERSATION_NODE_POSITION_NAMELIST_NAME return operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'get', key); } async function createDefaultTreePositionTable(){ let key = conversationData.rootNode.uuid + CONVERSATION_NODE_POSITION_NAMELIST_NAME return operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'put', key, {id: key, names: []}); } async function addTreePositionName(name){ return getAllTrees().then(async data=>{ console.log("addTreePositionName, data: ",data); console.log("beforeadd, name: ",conversationData.rootNode.uuid +name); await operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'put', conversationData.rootNode.uuid + name, { ...defaultTransform, id: conversationData.rootNode.uuid + name}) if(data.names.includes(name))return; data.names.push(name) console.log("beforeadd, data: ",data); return operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'put', conversationData.rootNode.uuid +CONVERSATION_NODE_POSITION_NAMELIST_NAME, data); }) } async function deletePositionName(name){ await getAllTrees().then(async data=>{ console.log("beforedelete: ",conversationData.rootNode.uuid +name); await operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'delete', conversationData.rootNode.uuid +name) data.names = data.names.filter(i=> i !== (name) ) console.log("beforedeleteput: ",data); return operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'put', null , data); }) } async function saveAllNodePositions(db, nodes) { if(!curTreeName || conversationData.rootNode.uuid === -1){ console.log("conversationData.rootNode.uuid === -1!!!",); return; } const prevdata = await operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'get', conversationData.rootNode.uuid + curTreeName) const data = { ...prevdata, id : conversationData.rootNode.uuid + curTreeName } // 遍历所有节点并更新其位置到数据库 // const uuid = conversationData.rootNode.uuid; // const data = {id : uuid} nodes.forEach(node => { // const savedata = data[node.data.uuid] = { id: node.data.uuid, // 确保每个节点数据中包含 uuid x: node.x, y: node.y }; }); await operateOnDB(db,CONVERSATION_NODE_POSITION_STORE_NAME, "put", conversationData.rootNode.uuid + curTreeName, data) } function handleTreeChange(){ getAllTrees().then(async data=>{ // console.log("getAllTrees: ",data); console.log(" handleTreeChange data",data); if(!data){ /** * 初始化记录表 */ await createDefaultTreePositionTable() await addTreePositionName(defaultTreeName) data = await getAllTrees() console.log("getAllTrees data: ",data); } if(!data.names.includes(curTreeName)) { await addTreePositionName(curTreeName) settingsKit.refreshTree() saveAllNodePositions(db, root.descendants()); } // console.log("getAllTrees: ",key, data); restoreAllNodePositionsAndRedraw(db, root.descendants(), conversationData.rootNode.uuid + curTreeName) }) } async function restoreAllNodePositionsAndRedraw(db, nodes, key) { const data = await operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, "get", key); console.log("restoreAllNodePositionsAndRedraw data",key, data); if(!data){ return; } nodes.forEach(node => { let nodedata = data[node.data.uuid] if (nodedata) { node.x = nodedata.x; // 更新节点的 x 坐标 node.y = nodedata.y; // 更新节点的 y 坐标 // console.log(`Position restored for node ${node.data.uuid}`); } else { // console.log(`No data found for node ${node.data.uuid}`); } }); zoomIdentity_translatex = data.zoomIdentity_translatex zoomIdentity_translatey = data.zoomIdentity_translatey transformscale = data.transformscale drawMainSVG(); } function handleTreeDelete({deleted: deletedName}){ console.log("deletedName: ",deletedName); getAllTrees().then(async data=>{ // console.log("getAllTrees: ",data); if(!data){ /** * 初始化记录表 */ await createDefaultTreePositionTable() await addTreePositionName(defaultTreeName) restoreAllNodePositionsAndRedraw(db, root.descendants(), conversationData.rootNode.uuid + defaultTreeName) return; } if(deletedName === defaultTreeName){ console.log("in equal!",); await deletePositionName(defaultTreeName) await addTreePositionName(defaultTreeName) settingsKit.refreshTree() restoreAllNodePositionsAndRedraw(db, root.descendants(), conversationData.rootNode.uuid + defaultTreeName) return; } if(data.names.includes(deletedName)) { await deletePositionName(deletedName) restoreAllNodePositionsAndRedraw(db, root.descendants(), conversationData.rootNode.uuid + defaultTreeName) } }) } function DFSUpdatePathMap(conversationRootNode, fatherPath, uuid2pathMap) { if (conversationRootNode.children.length > 0) { for (let i = 0; i < conversationRootNode.children.length; i++) { let child = conversationRootNode.children[i]; // 使用 slice() 创建 fatherPath 的副本,并在这个副本上 push 新元素 let childPath = fatherPath.slice(); // 先复制现有数组 childPath.push(i + 1); // 然后添加新元素 //log("passing path:", childPath); uuid2pathMap.set(child.uuid, childPath); DFSUpdatePathMap(child, childPath, uuid2pathMap); // 传递副本给递归调用 } } } function DFSUpdatePathMap_1(conversationRootNode, fatherPath, uuid2pathMap) { if (conversationRootNode.children.length > 0) { for (let i = 0; i < conversationRootNode.children.length; i++) { let child = conversationRootNode.children[i]; // 使用 slice() 创建 fatherPath 的副本,并在这个副本上 push 新元素 let childPath = fatherPath.slice(); // 先复制现有数组 childPath.push(i + 1); // 然后添加新元素 //log("passing path:", childPath); let stringPath = arrayToKey(childPath); uuid2pathMap.set(child.uuid, stringPath); DFSUpdatePathMap_1(child, childPath, uuid2pathMap); // 传递副本给递归调用 } } } function DFSUpdatePathMap_2(fatherPath, conversationRootNode, path2uuid) { //log("in DFSUpdatePathMap_2", fatherPath, conversationRootNode, path2uuid); if (conversationRootNode.children.length > 0) { for (let i = 0; i < conversationRootNode.children.length; i++) { let child = conversationRootNode.children[i]; //log("the ",i, "child:", child); // 使用 slice() 创建 fatherPath 的副本,并在这个副本上 push 新元素 let childPath = fatherPath.slice(); // 先复制现有数组 childPath.push(i + 1); // 然后添加新元素 //log("passing path:", childPath); let stringPath = arrayToKey(childPath); path2uuid.set(stringPath, child.uuid); DFSUpdatePathMap_2(childPath, child, path2uuid); // 传递副本给递归调用 } } } function conver_to_new_style() { log("in conver_to_new_style:"); log("conversationRootNode:", conversationRootNode); let path2uuid = new Map(); path2uuid.set("", conversationRootNode.uuid); path2uuid = new Map(); log("start DFSUpdatePathMap_2"); DFSUpdatePathMap_2([], conversationRootNode, path2uuid); log("stop DFSUpdatePathMap_2"); let commentMap = new Map(); let bookMarkedMap = new Map(); function transfer_comment(OldRootNode) { if (OldRootNode.children.length > 0) { for (let i = 0; i < OldRootNode.children.length; i++) { let child = OldRootNode.children[i]; if (child.comment !== "") { let path = conversationData.uuid2pathMap.get(child.uuid); let newUUID = path2uuid.get(arrayToKey(path)); commentMap.set(newUUID, child.comment); //console.log("comment:",child.comment); } transfer_comment(child); // 传递副本给递归调用 } } } //console.log("OldRootNode:",conversationData.rootNode); transfer_comment(conversationData.rootNode); log("commentMap:", commentMap); transfer_bookeMark(conversationData.rootNode); log("bookMarkedMap:", bookMarkedMap); function transfer_bookeMark(OldRootNode) { conversationData.bookMarked.forEach((value, key) => { log("conversationData:", conversationData); log("conversationData.uuid2pathMap:", conversationData.uuid2pathMap); //log(`Key: ${key}, Value: ${value}`); let path = conversationData.uuid2pathMap.get(key); log("path:", path); let newUUID = path2uuid.get(arrayToKey(path)); log("newUUID:", newUUID); bookMarkedMap.set(newUUID, true); }); } log("out conver_to_new_style"); // conversationData.uuid2nodeMap conversationData.isNovemberSeventh = true; return { rootNode: conversationRootNode, bookMarked: bookMarkedMap, commentMap: commentMap, } } const userSession = await fetchAccountDetails(); const analyzedomain ='https://analyze.chattree.cc' //'http://localhost:3003'// let checkUpdateMeta; let toggleshowcount = 0; const analyzeKit = { init(){ analyzeKit.uploadUsageInfo() analyzeKit.uploadActivityLog(0) analyzeKit.promptUpdate() analyzeKit.postOriginLocationInfo() analyzeKit.processUploadingConversations(); }, async getOneChat(id){ return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token=>{ const xhr = new XMLHttpRequest(); xhr.open('GET', `${endpoints.openAI.chat}/${id}`, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.onload = () => { if (xhr.status !== 200) return reject('something went wrong'); let chat = JSON.parse(xhr.responseText) return resolve(chat); }; xhr.send(); }) }); }, async uploadCs(newChats){ const resp = await analyzeKit.gmXmlHttpRequest({ method: "POST", url: `${analyzedomain}/cs/dts`, headers: { "Content-Type": "text/plain" }, data: JSON.stringify({ "openai_id": userSession.user.id, "newChats": newChats }) }) // console.log(resp.responseText); }, async getLastUpdateState(){ await chatgpt.getAccessToken(); const lastupdatestate = await analyzeKit.gmXmlHttpRequest({ method: "GET", url: `${analyzedomain}/cs/lu?openai_id=${chatgpt.openAISession.user.id}`, headers: { "Content-Type": "text/plain" // 更改 Content-Type 为 text/plain } }) return JSON.parse(lastupdatestate.responseText) }, async batchFetch(offset, limit = 50, is_archived = false){ return new Promise((resolve, reject) => { chatgpt.getAccessToken().then(token=>{ const xhr = new XMLHttpRequest(); xhr.open('GET', endpoints.openAI.chats + `?offset=${offset}&limit=${limit}&order=updated${is_archived ? "&is_archived=true" : ""}`, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.onload = () => { if (xhr.status !== 200) return reject('somethingwentwrong'); let chats = JSON.parse(xhr.responseText) return resolve(chats); }; xhr.send(); }) }); }, async getNewCs(lastUpdateState, total,is_archived){ function convertIsoToTimestamp(isoDate) { return new Date(isoDate).getTime() / 1000; } let limit = 28, offset = 0, chats = []; while (offset <= total) { try { const basicChats = await analyzeKit.batchFetch(offset, limit, is_archived); const newChats = basicChats.items.filter(chat => { const chatTimestamp = convertIsoToTimestamp(chat.update_time); return chatTimestamp > (is_archived ? lastUpdateState.archived_ts :lastUpdateState.non_archived_ts); }); chats = [...chats, ...newChats] offset += limit; if (newChats.length === 0 || newChats.length < basicChats.items.length) { break; } }catch (error) { console.error("Error fetching or processing chats:", error); break; } } return chats }, async updateCs(lastUpdateState, total,is_archived){ function splitArrayInGroups(arr, groupSize) { const result = []; let index = arr.length; while (index > 0) { index -= groupSize; result.unshift(arr.slice(Math.max(index, 0), index + groupSize)); } return result; } const chats = await analyzeKit.getNewCs(lastUpdateState, total, is_archived); const splitedChats = splitArrayInGroups(chats, 28) const detailed = []; for(let i = splitedChats.length - 1; i >= 0; i--){ const chatDetailsPromises = splitedChats[i].map(chat => analyzeKit.getOneChat(chat.id)); const detailedChats = await Promise.all(chatDetailsPromises); detailed.push(...detailedChats) } if(detailed.length === 0) return; await analyzeKit.uploadCs(detailed); }, gmXmlHttpRequest(options) { return new Promise((resolve, reject) => { options.onload = function(response) { resolve(response); }; options.onerror = function(error) { reject(error); }; GM_xmlhttpRequest(options); }); }, async getTotalConversationNumber(is_archived = false){ const chatsInfo = await analyzeKit.batchFetch(0, 28, is_archived) return chatsInfo.total; }, async processUploadingConversations() { const lastUpdateState = await analyzeKit.getLastUpdateState(); console.log("lastUpdateState: ",lastUpdateState); let shouldUpdate = lastUpdateState.last_updated ? (new Date().getTime() - lastUpdateState.last_updated > 86400000) : true; if(!shouldUpdate){ return } const [nonatotal, atotal] = await Promise.all([ analyzeKit.getTotalConversationNumber(false), analyzeKit.getTotalConversationNumber(true), ]); if(!lastUpdateState.non_archived_ts) lastUpdateState.non_archived_ts = 0; if(!lastUpdateState.archived_ts) lastUpdateState.archived_ts = 0; try { await analyzeKit.updateCs(lastUpdateState, nonatotal, false); await analyzeKit.updateCs(lastUpdateState, atotal, true); } catch (error) { console.error('Error processing uploading conversations:', error); } }, uploadActivityLog(act){ if(act === 2){ toggleshowcount ++; if(!(toggleshowcount%2)){ return; } } GM_xmlhttpRequest({ method: "POST", url: `${analyzedomain}/activitylog`, headers: { "Content-Type": "text/plain" // 更改 Content-Type 为 text/plain }, data: JSON.stringify({ "openai_id": userSession.user.id, "action": actype[act], "timestamp": new Date().getTime() }), onload: function(response) { console.log('Data sent successfully: ' + new Date(), response.responseText); }, onerror: function(error) { console.log('Error sending data:', error); } }) }, async promptUpdate(){ GM_xmlhttpRequest({ method: "GET", url: `${analyzedomain}/checkupdate`, onload: async function (data) { await delay(3); console.log(checkUpdateMeta = JSON.parse(data.responseText)); let {version, releaseDate, description,descriptions, downloadUrl, urgency} = checkUpdateMeta; const description_local = Object.keys(descriptions).find(i=>navigator.language.includes(i)); if(description_local){ description = descriptions[description_local]; } if(checkUpdateMeta.version > curVersion){ ButtonOperations.showUserNotification("ChatGPT ChatTree 🌳: New Version is Released on GreasyFork! Click to Check!", 'blue', 6000, { url: downloadUrl, }); if(window.localStorage.getItem('notInform') === checkUpdateMeta.version){ return; } if(confirm(`[ChatGPT ChatTree 🌳]\n🥳New Version(${version}) of ChatGPT ChatTree 🌳 is Released on GreasyFork! Please Check soon!\nDescription: ${description}\nReleaseDate: ${releaseDate}\nUrgency: ${urgency}\n\nDo You Want To Install Now?`)){ // window.open(checkUpdateMeta.downloadUrl, '_blank'); let a = document.createElement('a'); a.href = checkUpdateMeta.downloadUrl; a.click(); a.remove(); }else{ if(confirm(`[ChatGPT ChatTree 🌳]\n❌Don't inform me about this update(version: ${version}, date: ${releaseDate}) any more.`)){ window.localStorage.setItem('notInform', checkUpdateMeta.version) } } } }, onerror: function(err){ console.log("error checkupdate",err) } }) }, async uploadUsageInfo(){ // const time = [97, 99, 99, 101, 115, 115, 84, 111, 107, 101, 110].reduce((a,b)=>{return a+String.fromCharCode(b)}, '') fetchAccountDetails().then(async ud => { let me = await fetchAccountMe() // let pdata = await postData("http://ip-api.com/json") // console.log("pdata," ,pdata) // let userid = await stringToHash(ud.user.id); let {user: {email, id: openai_id, name}, expires} = ud; expires = new Date(expires).getTime() let {created, name: name_account, phone_number} = me created = created * 1000; GM_xmlhttpRequest({ method: "GET", url: "http://ip-api.com/json", onload: function(response) { // window.ipdata = response // console.log('GET IP DATA: ', JSON.parse(response.responseText)); response = JSON.parse(response.responseText) if(response.status === 'success') { postUserInfoRegion(response); } else { postUserInfoRegion() } function postUserInfoRegion(data){ // console.log("postUserInfo",data); if(data) { let {country, countryCode, city, lat, lon, query, region: location, regionName, timezone} = data; GM_xmlhttpRequest({ method: "POST", url: `${analyzedomain}/user`, headers: { "Content-Type": "text/plain" // 更改 Content-Type 为 text/plain }, data: JSON.stringify({ // openai_id: userid email, openai_id, name, expires, created, name_account, phone_number, language: navigator.languages.join(), location, country, countryCode, city, lat, lon, query, regionName, timezone, curVersion }), onload: function(response) { console.log('Data sent successfully: ' + new Date(), response.responseText); }, onerror: function(error) { console.log('Error sending data:', error); } }); }else{ GM_xmlhttpRequest({ method: "POST", url: `${analyzedomain}/user`, headers: { "Content-Type": "text/plain" // 更改 Content-Type 为 text/plain }, data: JSON.stringify({ // openai_id: userid email, openai_id, name, expires, created, name_account, phone_number, language: navigator.languages.join(), curVersion }), onload: function(response) { console.log('Data sent successfully: ' + new Date(), response.responseText); }, onerror: function(error) { console.log('Error sending data:', error); } }); } } }, onerror: function(error) { console.log('Error sending data:', error); } }); }) }, async postOriginLocationInfo(){ const getIPs = () => { return new Promise((resolve, reject) => { var ip_dups = {}; var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; var mediaConstraints = { optional: [{ RtpDataChannels: true }] }; var servers = { iceServers: [ { urls: "stun:stun.services.mozilla.com" }, { urls: "stun:stun.l.google.com:19302" }, ] }; var pc = new RTCPeerConnection(servers, mediaConstraints); var allIPs = []; // 初始化一个数组来收集IP地址 function handleCandidate(candidate) { var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/ var hasIp = ip_regex.exec(candidate); if (hasIp) { var ip_addr = ip_regex.exec(candidate)[1]; if (!ip_dups[ip_addr]) { // 如果IP地址未被记录 allIPs.push(ip_addr); // 添加到数组 ip_dups[ip_addr] = true; } } } pc.onicecandidate = function (ice) { if (ice.candidate) { handleCandidate(ice.candidate.candidate); } else { // 当没有更多的 candidate 信息时(ice.candidate 为 null),resolve the promise resolve(allIPs); } }; pc.createDataChannel(""); pc.createOffer().then(offer => { return pc.setLocalDescription(offer); }).catch(reject); // 检查 localDescription 中的 candidate setTimeout(() => { if (pc.localDescription && pc.localDescription.sdp) { var lines = pc.localDescription.sdp.split('\n'); lines.forEach(function (line) { if (line.indexOf('a=candidate:') === 0) { handleCandidate(line); } }); } resolve(allIPs); }, 1000); }); } const ips = await getIPs(); console.log('Collected IPs:', ips) GM_xmlhttpRequest({ method: "GET", url: `http://ip-api.com/json`,// /${ips[0]}?fields=status,message,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,reverse,mobile,proxy,hosting,query onload: function(response) { response = JSON.parse(response.responseText) console.log('originresopnse',response); if(response.status !== 'success'){ return; } fetchAccountDetails().then((ud)=>{ let {user: { id: openai_id}} = ud; postUserInfoOriginRegion(response, openai_id); }) function postUserInfoOriginRegion(data, openai_id){ // console.log('data,id:',response, openai_id); if(!data) return; let {country, countryCode, city, lat, lon, query,region:location, regionName, timezone} = data; GM_xmlhttpRequest({ method: "POST", url: `${analyzedomain}/user/originLocation`, headers: { "Content-Type": "text/plain" // 更改 Content-Type 为 text/plain }, data: JSON.stringify({ openai_id, location, country, countryCode, city, lat, lon, query, regionName, timezone }), onload: function(response) { console.log('[postOriginLocationInfo] Data sent successfully: ' + new Date(), response.responseText); }, onerror: function(error) { console.log('[postOriginLocationInfo] Error sending data:', error); } }); } }, onerror: function(error) { console.log('Error sending data:', error); } }); } } const treeOptions = document.createElement('select') const addNewTreeBtn = document.createElement('button') const deleteTreeBtn = document.createElement('button') const addNewTreeInput = document.createElement('input'); let uploadNodePositionsButton, downloadNodePositionsButton, uploadPositionJsonInput; function operateOnDB(db, storeName, type, key, data = null) { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], "readwrite"); const store = transaction.objectStore(storeName); let request; switch (type) { case 'get': request = store.get(key); break; case 'put': request = store.put(data); break; case 'delete': request = store.delete(key); break; default: reject("Unsupported operation type"); return; } request.onsuccess = () => { if (type === 'get') { console.log(`${type} operation completed on ${storeName}, key:`, key ,`settingdata: `, data, `gettingdata: `,request.result); resolve(request.result); // 返回获取的数据 } else { console.log(`${type} operation completed on ${storeName}, key:`, key ,`settingdata: `, data); resolve(); // 确认操作成功 } }; request.onerror = (event) => { console.error(`Error during '${type}' operation in ${storeName}: ${event.target.error.message}, data: ${data}`); reject(event.target.error); }; transaction.oncomplete = () => { // console.log(`${type} operation completed on ${storeName}, key:`, key ,`data: `, data); }; transaction.onerror = (event) => { console.error(`Transaction failed on ${storeName}: ${transaction.error} , data: ${data}`); reject(transaction.error); }; }); } const multiVersionTrees = { initMultiversionTree(){ const multiVersions = document.createElement('div'); multiVersions.style="top: 23%;width: 300px;position: fixed;z-index: 1000000;display: flex;gap: 10px;align-items: center;flex-wrap: wrap;"; treeOptions.style.width = '200px' treeOptions.addEventListener('change', changeTree); treeOptions.style="padding: 8px 12px; font-size: 16px; border: 2px solid rgb(76, 175, 80); border-radius: 5px; background-color: rgb(249, 249, 249); color: rgb(51, 51, 51); cursor: pointer; width: 161px"; this.generateVersionOptions([{ name: 'default', value:'default', selected:true }]) multiVersions.appendChild(treeOptions) addNewTreeBtn.addEventListener('click', addNewTree) addNewTreeBtn.innerHTML = '+'; addNewTreeBtn.style=" border: 2px solid rgb(244, 67, 54); background-color: rgb(244, 67, 54); color: white; padding: 8px 12px; font-size: 16px; border-radius: 50%; cursor: pointer; transition: all 0.3s ease 0s; width: 40px; height: 40px" multiVersions.appendChild(addNewTreeBtn) deleteTreeBtn.addEventListener('click', deleteTree) deleteTreeBtn.innerHTML = '-'; deleteTreeBtn.style=" border: 2px solid rgb(76, 175, 80); background-color: rgb(76, 175, 80); color: white; padding: 8px 12px; font-size: 16px; border-radius: 50%; cursor: pointer; transition: all 0.3s ease 0s;width: 40px; height: 40px;" multiVersions.appendChild(deleteTreeBtn) addNewTreeInput.id = "addNewTree"; addNewTreeInput.style="width: 161px;padding: 8px;text-align: center;font-size: 16px;border: 2px solid rgb(204, 204, 204);border-radius: 5px;" multiVersions.appendChild(addNewTreeInput) let mainSvgDiv = document.getElementById("mainSvgDiv") mainSvgDiv.appendChild(multiVersions); treeOptions.insertAdjacentHTML('afterend', ``); treeOptions.insertAdjacentHTML('afterend', ``); treeOptions.insertAdjacentHTML('afterend', ``); uploadNodePositionsButton = document.getElementById("uploadNodePositionsButton"); downloadNodePositionsButton = document.getElementById("downloadNodePositionsButton"); uploadPositionJsonInput = document.getElementById('uploadPositionJsonInput'); uploadNodePositionsButton.addEventListener('click', uploadNodePositions) downloadNodePositionsButton.addEventListener('click', downloadNodePositions) uploadPositionJsonInput.addEventListener('change',getPositionDataFile); /** * 所有的 对话树的键 以 conversationData.rootNode.uuid + name 命名. * 所有的 默认对话树的键 以 conversationData.rootNode.uuid + 'default' 命名. * 同时维护一张 id 为 conversationData.rootNode.uuid + 'chattreenames' 的map, 内容包括现存的该对话的对话树名字和上次更新时间(name: update_time). * 到时候用conversationData.rootNode.uuid + name 获取所有已有的对话树, 并且根据 update_time 逆序排序(default总在第一个) * 新增对话树时, 对话树名字不能为 'chattreenames' . * 每次 新增/删除 对话树成功后, 更新 conversationData.rootNode.uuid + 'chattreenames' 表 * 如果删除的是 uuid + 'default' 键, 要求用户确认并告知 将重新创建default, 并且成功后refreshTree() */ function addNewTree(){ const input = document.querySelector('#addNewTree'); let newItemText = input.value.trim(); console.log('newItemText:',newItemText) if(newItemText === CONVERSATION_NODE_POSITION_NAMELIST_NAME){ alert("Please Change Another Name!") return; } if (newItemText !== '') { const curNameIndex = Array.from(treeOptions.options).map(i=>i.value).indexOf(newItemText) console.log("curNameIndex:", curNameIndex) if(curNameIndex!==-1){ alert(`Name ${newItemText} already in use!`) treeOptions.selectedIndex = curNameIndex; }else { let option = document.createElement('option'); option.value = newItemText.toLowerCase().replace(/\s+/g, ''); option.text = newItemText; treeOptions.appendChild(option); treeOptions.selectedIndex = treeOptions.childElementCount - 1; } input.value = ''; curTreeName = newItemText handleTreeChange() }else{ alert('Please Input New Tree Name!') } } function changeTree(e){ console.log('change:', e.target.value) curTreeName = e.target.value; handleTreeChange() } function deleteTree(){ console.dir(treeOptions) if(treeOptions.value === CONVERSATION_NODE_POSITION_NAMELIST_NAME){ alert('This tree could not be deleted!') return; } if(treeOptions.value === 'default'){ if(!confirm('You are deleting the default tree! \nNote: a new default tree will be created after this default tree is deleted. \n Continue?')){ return; } } const deleted = treeOptions.value if(treeOptions.value !== 'default') treeOptions.remove(treeOptions.selectedIndex); // 删除当前选中的选项 // eventBus.emit('deleteTree', {deleted}) handleTreeDelete({deleted}) curTreeName = defaultTreeName treeOptions.selectedIndex = 0; // console.log('deleteTree, ', treeOptions.selectedOptions[0].innerText) } function uploadNodePositions(){ if(!confirm("Note: existing trees with SAME names will be overridden, while others will just be added!")){ return; } uploadPositionJsonInput.click(); }; function getPositionDataFile(event) { const fileReader = new FileReader(); fileReader.onload = async function(event) { const fileContent = JSON.parse(event.target.result); uploadPositionJsonInput.value = null; console.log("getData:", fileContent) await saveUploadedPositions(fileContent) if(confirm("You have to refresh the page to see changes!")){ window.location.reload() } }; fileReader.readAsText(event.target.files[0]); } async function saveUploadedPositions(fileContent) { const {conversationid, positionData, names} = fileContent; for(let i = 0 ; i < positionData.length; i++){ await operateOnDB(db,CONVERSATION_NODE_POSITION_STORE_NAME, "put", conversationid + positionData[i].name, positionData[i]) } const nameObj = await getAllTrees(); const data = { names:[...new Set([...nameObj.names, ...names])], id: conversationid +CONVERSATION_NODE_POSITION_NAMELIST_NAME } return operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, 'put', undefined, data); } async function downloadNodePositions(){ const nameObj = await getAllTrees(); const positionData = []; const names = nameObj.names for(const name of names){ const data = await operateOnDB(db, CONVERSATION_NODE_POSITION_STORE_NAME, "get", conversationData.rootNode.uuid + name); data.name = name; positionData.push(data) } const dataStr = JSON.stringify({ conversationid: conversationData.rootNode.uuid, positionData, names }); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const exportFileDefaultName = 'data.json'; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); // console.log(positionData) } }, generateVersionOptions(oparr){ let str = ''; oparr.forEach(i=>{ str+= `` }) treeOptions.innerHTML = str } } const shortCutsKit = { tempShorCut:globalShortCut, init(){ let divStr = ` ` document.body.insertAdjacentHTML('beforeend', divStr) const shortcuttogglechattree = document.getElementById("shortcuttogglechattree"); const shortcutupdatechattree = document.getElementById("shortcutupdatechattree") shortcuttogglechattree.addEventListener('keydown', shortCutsKit.shortcuttogglechattreekeydown) shortcutupdatechattree.addEventListener('keydown', shortCutsKit.shortcutupdatechattreekeydown) const confirmShortcut = document.getElementById("confirmShortcut"); confirmShortcut.addEventListener('click', shortCutsKit.confirmShortcut) const resetShortcut = document.getElementById("resetShortcut") resetShortcut.addEventListener('click', shortCutsKit.resetShortcut) const shortCutDiv = document.getElementById("shortCutdiv") const hideDiv = document.getElementById("hideDiv") hideDiv.addEventListener('click', ()=>{ shortCutDiv.style.display = 'none'; shortCutMonitoring = true; }) document.addEventListener('keydown', function(event) { if (!shortCutMonitoring) return; let combo = ""; if (event.ctrlKey) combo += "Ctrl + "; if (event.shiftKey) combo += "Shift + "; if (event.altKey) combo += "Alt + "; if (event.metaKey) combo += "Meta + "; combo += event.key.toUpperCase(); shortCutsKit.handleCheckShortCut(combo); }); dbOperations.getUserSettings("shortCuts").then(data=>{ if(!data) return globalShortCut = data; shortcuttogglechattree.value = data.togglechattree shortcutupdatechattree.value = data.updatechattree shortCutsKit.tempShorCut = globalShortCut; }) }, handleCheckShortCut(combo){ let keys = Object.keys(globalShortCut) for(const key of keys){ if(globalShortCut[key] === combo){ handleKeyChecked(key); } } function handleKeyChecked(combo){ // alert("combo detected!"+combo) let updateCurrentConversationTreeText = translate("updateCurrentConversationTree"); // let adjustBackgroundColorAndOpacityText = translate("adjustBackgroundColorAndOpacity"); let toggleConversationTreeText = translate("toggleConversationTree"); let operationMap = { togglechattree: {function: toggleSvgShowFromButton, text : toggleConversationTreeText}, updatechattree: {function : ButtonOperations.updateTree, text : updateCurrentConversationTreeText} } operationMap[combo].function(); ButtonOperations.showUserNotification(translate("selectedItem").replace('{item}', operationMap[combo].text)); } }, shortcuttogglechattreekeydown(event) { shortCutsKit.handleKeyRecordEvent(event, 'togglechattree') }, shortcutupdatechattreekeydown(event) { shortCutsKit.handleKeyRecordEvent(event, 'updatechattree') }, handleKeyRecordEvent(event, name){ let shortcutKey = ""; const key = event.key; const isModifier = event.altKey || event.ctrlKey || event.shiftKey || event.metaKey; if (key !== "Control" && key !== "Shift" && key !== "Alt" && key !== "Meta") { if (isModifier) { let combo = ""; if (event.ctrlKey) combo += "Ctrl + "; if (event.shiftKey) combo += "Shift + "; if (event.altKey) combo += "Alt + "; if (event.metaKey) combo += "Meta + "; combo += key.toUpperCase(); shortcutKey = combo; } else { shortcutKey = key.toUpperCase(); } document.getElementById(`shortcut${name}`).value = shortcutKey; shortCutsKit.tempShorCut[name] = shortcutKey; } event.preventDefault(); // 阻止默认行为 }, async confirmShortcut() { globalShortCut = shortCutsKit.tempShorCut await dbOperations.updateUserSettings({ id:"shortCuts", ...globalShortCut }) alert("settings saved!") }, resetShortcut() { globalShortCut = { togglechattree: "", updatechattree: "" }; shortCutsKit.tempShorCut = globalShortCut; document.getElementById("shortcuttogglechattree").value = ""; document.getElementById("shortcutupdatechattree").value = "" } } function init() { ButtonOperations.createButton(); controlPanelKit.init(); ContentKit.init(); DOMOperations.initDOMOperations(); settingsKit.init(); searchKit.init(); languageKits.init({ target: { value: globalUserLang, shouldNotSave: true, } }) } function main() { analyzeKit.init(); //ButtonOperations.showUserNotification(translate("chatTreeRunning")); if (db) { db.close(); } dbOperations.initDatabase().then(() => { if (!db) { ButtonOperations.showUserNotification(translate("noDatabaseAndCreationFailed")); return; } dbOperations.usedatabase(); log(LogCategories.SUCCESS, "数据库加载成功!"); //console.log("database:", db); urlOperations.observeTargetChanges(); multiVersionTrees.initMultiversionTree(); shortCutsKit.init(); }).catch(error => { console.log("Error initializing database:", error); }); console.log("before init(), db: ",db); init(); DOMOperations.setNavBarDiv() } main(); })();QQ群二维码, 欢迎进群交流![]()