diff --git a/messages/ar.json b/messages/ar.json index 891760d2..d5210f16 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "عند حفظ الأوامر عبر MCP، اجعلها عامة افتراضياً بدلاً من خاصة.", "settingUpdated": "تم تحديث الإعداد" }, + "connectedPrompts": { + "title": "تدفق الأوامر", + "description": "بعض الأوامر تعمل كسير عمل متعدد الخطوات. قم بربط أوامرك لإنشاء تسلسل.", + "addConnection": "إضافة اتصال", + "addPrevious": "إضافة سابق", + "addNext": "إضافة تالي", + "addConnectionTitle": "إضافة اتصال", + "addPreviousTitle": "إضافة أمر سابق", + "addNextTitle": "إضافة أمر تالي", + "addConnectionDescription": "ربط هذا الأمر بأمر آخر مع تسمية شرط.", + "addPreviousDescription": "اختر أمراً يأتي قبل هذا في سير العمل.", + "addNextDescription": "اختر أمراً يأتي بعد هذا في سير العمل.", + "noConnections": "لا توجد أوامر متصلة بعد. أضف اتصالات لإنشاء سلسلة أوامر.", + "previousSteps": "الخطوات السابقة", + "nextSteps": "الخطوات التالية", + "searchPrompt": "بحث عن أمر", + "searchPlaceholder": "البحث بالعنوان...", + "selectedPrompt": "الأمر المحدد", + "connectionLabel": "تسمية الاتصال", + "labelPlaceholder": "مثال: الإطار الأول، الخطوة التالية، بعد المعالجة...", + "labelHint": "صف الشرط أو الانتقال بين الأوامر", + "change": "تغيير", + "cancel": "إلغاء", + "fillAllFields": "يرجى اختيار أمر وإدخال تسمية", + "connectionFailed": "فشل إنشاء الاتصال", + "connectionAdded": "تمت إضافة الاتصال بنجاح", + "connectionDeleted": "تم حذف الاتصال", + "deleteFailed": "فشل حذف الاتصال", + "noResults": "لم يتم العثور على أوامر" + }, "promptWritingGuide": { "title": "كيفية كتابة أوامر رائعة", "subtitle": "نصائح وأمثلة وأفضل الممارسات لإنشاء أوامر ذكاء اصطناعي فعالة", diff --git a/messages/de.json b/messages/de.json index 3d3ee237..ebbea37a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Beim Speichern von Prompts über MCP standardmäßig öffentlich statt privat machen.", "settingUpdated": "Einstellung aktualisiert" }, + "connectedPrompts": { + "title": "Prompt-Ablauf", + "description": "Einige Prompts funktionieren als mehrstufige Workflows. Verbinden Sie Ihre Prompts, um eine Sequenz zu erstellen.", + "addConnection": "Verbindung hinzufügen", + "addPrevious": "Vorherigen hinzufügen", + "addNext": "Nächsten hinzufügen", + "addConnectionTitle": "Verbindung hinzufügen", + "addPreviousTitle": "Vorherigen Prompt hinzufügen", + "addNextTitle": "Nächsten Prompt hinzufügen", + "addConnectionDescription": "Verknüpfen Sie diesen Prompt mit einem anderen Prompt mit einer Bedingungsbezeichnung.", + "addPreviousDescription": "Wählen Sie einen Prompt, der vor diesem im Workflow kommt.", + "addNextDescription": "Wählen Sie einen Prompt, der nach diesem im Workflow kommt.", + "noConnections": "Noch keine verbundenen Prompts. Fügen Sie Verbindungen hinzu, um eine Prompt-Kette zu erstellen.", + "previousSteps": "Vorherige Schritte", + "nextSteps": "Nächste Schritte", + "searchPrompt": "Prompt suchen", + "searchPlaceholder": "Nach Titel suchen...", + "selectedPrompt": "Ausgewählter Prompt", + "connectionLabel": "Verbindungsbezeichnung", + "labelPlaceholder": "z.B. erster Rahmen, nächster Schritt, nach Verarbeitung...", + "labelHint": "Beschreiben Sie die Bedingung oder den Übergang zwischen Prompts", + "change": "Ändern", + "cancel": "Abbrechen", + "fillAllFields": "Bitte wählen Sie einen Prompt und geben Sie eine Bezeichnung ein", + "connectionFailed": "Verbindung konnte nicht erstellt werden", + "connectionAdded": "Verbindung erfolgreich hinzugefügt", + "connectionDeleted": "Verbindung gelöscht", + "deleteFailed": "Verbindung konnte nicht gelöscht werden", + "noResults": "Keine Prompts gefunden" + }, "promptWritingGuide": { "title": "Wie Man Gute Prompts Schreibt", "subtitle": "Tipps, Beispiele und Best Practices für effektive KI-Prompts", diff --git a/messages/el.json b/messages/el.json index 6d189a49..ac44b8f7 100644 --- a/messages/el.json +++ b/messages/el.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Κατά την αποθήκευση prompts μέσω MCP, να γίνονται δημόσια από προεπιλογή αντί για ιδιωτικά.", "settingUpdated": "Η ρύθμιση ενημερώθηκε" }, + "connectedPrompts": { + "title": "Ροή Prompts", + "description": "Ορισμένα prompts λειτουργούν ως ροές εργασίας πολλαπλών βημάτων. Συνδέστε τα prompts σας για να δημιουργήσετε μια ακολουθία.", + "addConnection": "Προσθήκη Σύνδεσης", + "addPrevious": "Προσθήκη Προηγούμενου", + "addNext": "Προσθήκη Επόμενου", + "addConnectionTitle": "Προσθήκη Σύνδεσης", + "addPreviousTitle": "Προσθήκη Προηγούμενου Prompt", + "addNextTitle": "Προσθήκη Επόμενου Prompt", + "addConnectionDescription": "Συνδέστε αυτό το prompt με ένα άλλο με μια ετικέτα συνθήκης.", + "addPreviousDescription": "Επιλέξτε ένα prompt που έρχεται πριν από αυτό στη ροή εργασίας.", + "addNextDescription": "Επιλέξτε ένα prompt που έρχεται μετά από αυτό στη ροή εργασίας.", + "noConnections": "Δεν υπάρχουν συνδεδεμένα prompts ακόμα. Προσθέστε συνδέσεις για να δημιουργήσετε μια αλυσίδα prompts.", + "previousSteps": "Προηγούμενα βήματα", + "nextSteps": "Επόμενα βήματα", + "searchPrompt": "Αναζήτηση Prompt", + "searchPlaceholder": "Αναζήτηση με τίτλο...", + "selectedPrompt": "Επιλεγμένο Prompt", + "connectionLabel": "Ετικέτα Σύνδεσης", + "labelPlaceholder": "π.χ. πρώτο καρέ, επόμενο βήμα, μετά την επεξεργασία...", + "labelHint": "Περιγράψτε τη συνθήκη ή τη μετάβαση μεταξύ prompts", + "change": "Αλλαγή", + "cancel": "Ακύρωση", + "fillAllFields": "Παρακαλώ επιλέξτε ένα prompt και εισάγετε μια ετικέτα", + "connectionFailed": "Αποτυχία δημιουργίας σύνδεσης", + "connectionAdded": "Η σύνδεση προστέθηκε επιτυχώς", + "connectionDeleted": "Η σύνδεση διαγράφηκε", + "deleteFailed": "Αποτυχία διαγραφής σύνδεσης", + "noResults": "Δεν βρέθηκαν prompts" + }, "promptWritingGuide": { "title": "Πώς να Γράφετε Εξαιρετικά Prompts", "subtitle": "Συμβουλές, παραδείγματα και βέλτιστες πρακτικές για τη δημιουργία αποτελεσματικών AI prompts", diff --git a/messages/en.json b/messages/en.json index ae927418..61b64d21 100644 --- a/messages/en.json +++ b/messages/en.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "When saving prompts via MCP, make them public by default instead of private.", "settingUpdated": "Setting updated" }, + "connectedPrompts": { + "title": "Prompt Flow", + "description": "Some prompts work as multi-step workflows. Connect your prompts to create a sequence.", + "addConnection": "Add Connection", + "addPrevious": "Add Previous", + "addNext": "Add Next", + "addConnectionTitle": "Add Connection", + "addPreviousTitle": "Add Previous Prompt", + "addNextTitle": "Add Next Prompt", + "addConnectionDescription": "Link this prompt to another prompt with a condition label.", + "addPreviousDescription": "Select a prompt that comes before this one in the workflow.", + "addNextDescription": "Select a prompt that comes after this one in the workflow.", + "noConnections": "No connected prompts yet. Add connections to create a prompt chain.", + "previousSteps": "Previous steps", + "nextSteps": "Next steps", + "searchPrompt": "Search Prompt", + "searchPlaceholder": "Search by title...", + "selectedPrompt": "Selected Prompt", + "connectionLabel": "Connection Label", + "labelPlaceholder": "e.g., first frame, next step, after processing...", + "labelHint": "Describe the condition or transition between prompts", + "change": "Change", + "cancel": "Cancel", + "fillAllFields": "Please select a prompt and enter a label", + "connectionFailed": "Failed to create connection", + "connectionAdded": "Connection added successfully", + "connectionDeleted": "Connection deleted", + "deleteFailed": "Failed to delete connection", + "noResults": "No prompts found" + }, "promptWritingGuide": { "title": "How to Write Great Prompts", "subtitle": "Tips, examples, and best practices for creating effective AI prompts", diff --git a/messages/es.json b/messages/es.json index 90630a53..36c3d51e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Al guardar prompts vía MCP, hacerlos públicos por defecto en lugar de privados.", "settingUpdated": "Configuración actualizada" }, + "connectedPrompts": { + "title": "Flujo de Prompts", + "description": "Algunos prompts funcionan como flujos de trabajo de varios pasos. Conecta tus prompts para crear una secuencia.", + "addConnection": "Añadir Conexión", + "addPrevious": "Añadir Anterior", + "addNext": "Añadir Siguiente", + "addConnectionTitle": "Añadir Conexión", + "addPreviousTitle": "Añadir Prompt Anterior", + "addNextTitle": "Añadir Prompt Siguiente", + "addConnectionDescription": "Vincula este prompt con otro prompt con una etiqueta de condición.", + "addPreviousDescription": "Selecciona un prompt que viene antes de este en el flujo de trabajo.", + "addNextDescription": "Selecciona un prompt que viene después de este en el flujo de trabajo.", + "noConnections": "Aún no hay prompts conectados. Añade conexiones para crear una cadena de prompts.", + "previousSteps": "Pasos anteriores", + "nextSteps": "Siguientes pasos", + "searchPrompt": "Buscar Prompt", + "searchPlaceholder": "Buscar por título...", + "selectedPrompt": "Prompt Seleccionado", + "connectionLabel": "Etiqueta de Conexión", + "labelPlaceholder": "ej. primer fotograma, siguiente paso, después de procesar...", + "labelHint": "Describe la condición o transición entre prompts", + "change": "Cambiar", + "cancel": "Cancelar", + "fillAllFields": "Por favor selecciona un prompt e introduce una etiqueta", + "connectionFailed": "Error al crear la conexión", + "connectionAdded": "Conexión añadida exitosamente", + "connectionDeleted": "Conexión eliminada", + "deleteFailed": "Error al eliminar la conexión", + "noResults": "No se encontraron prompts" + }, "promptWritingGuide": { "title": "Cómo Escribir Buenos Prompts", "subtitle": "Consejos, ejemplos y mejores prácticas para crear prompts de IA efectivos", diff --git a/messages/fr.json b/messages/fr.json index 4cdd19f8..70297625 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Lors de l'enregistrement de prompts via MCP, les rendre publics par défaut au lieu de privés.", "settingUpdated": "Paramètre mis à jour" }, + "connectedPrompts": { + "title": "Flux de Prompts", + "description": "Certains prompts fonctionnent comme des workflows multi-étapes. Connectez vos prompts pour créer une séquence.", + "addConnection": "Ajouter une Connexion", + "addPrevious": "Ajouter Précédent", + "addNext": "Ajouter Suivant", + "addConnectionTitle": "Ajouter une Connexion", + "addPreviousTitle": "Ajouter Prompt Précédent", + "addNextTitle": "Ajouter Prompt Suivant", + "addConnectionDescription": "Liez ce prompt à un autre prompt avec une étiquette de condition.", + "addPreviousDescription": "Sélectionnez un prompt qui vient avant celui-ci dans le workflow.", + "addNextDescription": "Sélectionnez un prompt qui vient après celui-ci dans le workflow.", + "noConnections": "Pas encore de prompts connectés. Ajoutez des connexions pour créer une chaîne de prompts.", + "previousSteps": "Étapes précédentes", + "nextSteps": "Étapes suivantes", + "searchPrompt": "Rechercher un Prompt", + "searchPlaceholder": "Rechercher par titre...", + "selectedPrompt": "Prompt Sélectionné", + "connectionLabel": "Étiquette de Connexion", + "labelPlaceholder": "ex. première image, étape suivante, après traitement...", + "labelHint": "Décrivez la condition ou la transition entre les prompts", + "change": "Changer", + "cancel": "Annuler", + "fillAllFields": "Veuillez sélectionner un prompt et entrer une étiquette", + "connectionFailed": "Échec de la création de la connexion", + "connectionAdded": "Connexion ajoutée avec succès", + "connectionDeleted": "Connexion supprimée", + "deleteFailed": "Échec de la suppression de la connexion", + "noResults": "Aucun prompt trouvé" + }, "promptWritingGuide": { "title": "Comment Écrire de Bons Prompts", "subtitle": "Conseils, exemples et meilleures pratiques pour créer des prompts IA efficaces", diff --git a/messages/he.json b/messages/he.json index d1413438..fa4bdcc9 100644 --- a/messages/he.json +++ b/messages/he.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "בעת שמירת פרומפטים דרך MCP, הפוך אותם לציבוריים כברירת מחדל במקום פרטיים.", "settingUpdated": "ההגדרה עודכנה" }, + "connectedPrompts": { + "title": "זרימת פרומפטים", + "description": "חלק מהפרומפטים עובדים כתהליכי עבודה מרובי שלבים. חבר את הפרומפטים שלך ליצירת רצף.", + "addConnection": "הוסף קישור", + "addPrevious": "הוסף קודם", + "addNext": "הוסף הבא", + "addConnectionTitle": "הוסף קישור", + "addPreviousTitle": "הוסף פרומפט קודם", + "addNextTitle": "הוסף פרומפט הבא", + "addConnectionDescription": "קשר פרומפט זה לפרומפט אחר עם תווית תנאי.", + "addPreviousDescription": "בחר פרומפט שבא לפני זה בתהליך העבודה.", + "addNextDescription": "בחר פרומפט שבא אחרי זה בתהליך העבודה.", + "noConnections": "אין עדיין פרומפטים מקושרים. הוסף קישורים ליצירת שרשרת פרומפטים.", + "previousSteps": "שלבים קודמים", + "nextSteps": "שלבים הבאים", + "searchPrompt": "חפש פרומפט", + "searchPlaceholder": "חפש לפי כותרת...", + "selectedPrompt": "פרומפט נבחר", + "connectionLabel": "תווית קישור", + "labelPlaceholder": "לדוגמה: פריים ראשון, שלב הבא, לאחר עיבוד...", + "labelHint": "תאר את התנאי או המעבר בין הפרומפטים", + "change": "שנה", + "cancel": "ביטול", + "fillAllFields": "אנא בחר פרומפט והזן תווית", + "connectionFailed": "יצירת הקישור נכשלה", + "connectionAdded": "הקישור נוסף בהצלחה", + "connectionDeleted": "הקישור נמחק", + "deleteFailed": "מחיקת הקישור נכשלה", + "noResults": "לא נמצאו פרומפטים" + }, "promptWritingGuide": { "title": "איך לכתוב פרומפטים מעולים", "subtitle": "טיפים, דוגמאות ושיטות עבודה מומלצות ליצירת פרומפטים יעילים לבינה מלאכותית", diff --git a/messages/it.json b/messages/it.json index 6d5efe6c..4f60658d 100644 --- a/messages/it.json +++ b/messages/it.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Quando salvi prompt tramite MCP, rendili pubblici per impostazione predefinita invece che privati.", "settingUpdated": "Impostazione aggiornata" }, + "connectedPrompts": { + "title": "Flusso Prompt", + "description": "Alcuni prompt funzionano come workflow a più passaggi. Collega i tuoi prompt per creare una sequenza.", + "addConnection": "Aggiungi Connessione", + "addPrevious": "Aggiungi Precedente", + "addNext": "Aggiungi Successivo", + "addConnectionTitle": "Aggiungi Connessione", + "addPreviousTitle": "Aggiungi Prompt Precedente", + "addNextTitle": "Aggiungi Prompt Successivo", + "addConnectionDescription": "Collega questo prompt a un altro prompt con un'etichetta di condizione.", + "addPreviousDescription": "Seleziona un prompt che viene prima di questo nel workflow.", + "addNextDescription": "Seleziona un prompt che viene dopo questo nel workflow.", + "noConnections": "Nessun prompt collegato ancora. Aggiungi connessioni per creare una catena di prompt.", + "previousSteps": "Passaggi precedenti", + "nextSteps": "Passaggi successivi", + "searchPrompt": "Cerca Prompt", + "searchPlaceholder": "Cerca per titolo...", + "selectedPrompt": "Prompt Selezionato", + "connectionLabel": "Etichetta Connessione", + "labelPlaceholder": "es. primo fotogramma, passaggio successivo, dopo l'elaborazione...", + "labelHint": "Descrivi la condizione o la transizione tra i prompt", + "change": "Cambia", + "cancel": "Annulla", + "fillAllFields": "Seleziona un prompt e inserisci un'etichetta", + "connectionFailed": "Impossibile creare la connessione", + "connectionAdded": "Connessione aggiunta con successo", + "connectionDeleted": "Connessione eliminata", + "deleteFailed": "Impossibile eliminare la connessione", + "noResults": "Nessun prompt trovato" + }, "promptWritingGuide": { "title": "Come Scrivere Buoni Prompt", "subtitle": "Suggerimenti, esempi e best practice per creare prompt IA efficaci", diff --git a/messages/ja.json b/messages/ja.json index 3f05a32a..98749181 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "MCP経由でプロンプトを保存する際、プライベートではなくデフォルトで公開にします。", "settingUpdated": "設定が更新されました" }, + "connectedPrompts": { + "title": "プロンプトフロー", + "description": "一部のプロンプトは複数ステップのワークフローとして機能します。プロンプトを接続してシーケンスを作成しましょう。", + "addConnection": "接続を追加", + "addPrevious": "前を追加", + "addNext": "次を追加", + "addConnectionTitle": "接続を追加", + "addPreviousTitle": "前のプロンプトを追加", + "addNextTitle": "次のプロンプトを追加", + "addConnectionDescription": "このプロンプトを条件ラベル付きで別のプロンプトにリンクします。", + "addPreviousDescription": "ワークフローでこれより前に来るプロンプトを選択してください。", + "addNextDescription": "ワークフローでこれより後に来るプロンプトを選択してください。", + "noConnections": "接続されたプロンプトはまだありません。プロンプトチェーンを作成するには接続を追加してください。", + "previousSteps": "前のステップ", + "nextSteps": "次のステップ", + "searchPrompt": "プロンプトを検索", + "searchPlaceholder": "タイトルで検索...", + "selectedPrompt": "選択したプロンプト", + "connectionLabel": "接続ラベル", + "labelPlaceholder": "例:最初のフレーム、次のステップ、処理後...", + "labelHint": "プロンプト間の条件または遷移を説明してください", + "change": "変更", + "cancel": "キャンセル", + "fillAllFields": "プロンプトを選択してラベルを入力してください", + "connectionFailed": "接続の作成に失敗しました", + "connectionAdded": "接続が正常に追加されました", + "connectionDeleted": "接続が削除されました", + "deleteFailed": "接続の削除に失敗しました", + "noResults": "プロンプトが見つかりません" + }, "promptWritingGuide": { "title": "優れたプロンプトの書き方", "subtitle": "効果的なAIプロンプトを作成するためのヒント、例、ベストプラクティス", diff --git a/messages/ko.json b/messages/ko.json index 366de083..18a95706 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "MCP를 통해 프롬프트를 저장할 때 비공개 대신 기본적으로 공개로 설정합니다.", "settingUpdated": "설정이 업데이트되었습니다" }, + "connectedPrompts": { + "title": "프롬프트 흐름", + "description": "일부 프롬프트는 다단계 워크플로로 작동합니다. 프롬프트를 연결하여 시퀀스를 만드세요.", + "addConnection": "연결 추가", + "addPrevious": "이전 추가", + "addNext": "다음 추가", + "addConnectionTitle": "연결 추가", + "addPreviousTitle": "이전 프롬프트 추가", + "addNextTitle": "다음 프롬프트 추가", + "addConnectionDescription": "이 프롬프트를 조건 레이블과 함께 다른 프롬프트에 연결합니다.", + "addPreviousDescription": "워크플로에서 이것보다 먼저 오는 프롬프트를 선택하세요.", + "addNextDescription": "워크플로에서 이것 다음에 오는 프롬프트를 선택하세요.", + "noConnections": "아직 연결된 프롬프트가 없습니다. 프롬프트 체인을 만들려면 연결을 추가하세요.", + "previousSteps": "이전 단계", + "nextSteps": "다음 단계", + "searchPrompt": "프롬프트 검색", + "searchPlaceholder": "제목으로 검색...", + "selectedPrompt": "선택된 프롬프트", + "connectionLabel": "연결 레이블", + "labelPlaceholder": "예: 첫 번째 프레임, 다음 단계, 처리 후...", + "labelHint": "프롬프트 간의 조건 또는 전환을 설명하세요", + "change": "변경", + "cancel": "취소", + "fillAllFields": "프롬프트를 선택하고 레이블을 입력하세요", + "connectionFailed": "연결 생성에 실패했습니다", + "connectionAdded": "연결이 성공적으로 추가되었습니다", + "connectionDeleted": "연결이 삭제되었습니다", + "deleteFailed": "연결 삭제에 실패했습니다", + "noResults": "프롬프트를 찾을 수 없습니다" + }, "promptWritingGuide": { "title": "좋은 프롬프트 작성법", "subtitle": "효과적인 AI 프롬프트를 만들기 위한 팁, 예시, 모범 사례", diff --git a/messages/pt.json b/messages/pt.json index c9cc8976..fde8ec7e 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "Ao salvar prompts via MCP, torná-los públicos por padrão em vez de privados.", "settingUpdated": "Configuração atualizada" }, + "connectedPrompts": { + "title": "Fluxo de Prompts", + "description": "Alguns prompts funcionam como fluxos de trabalho de várias etapas. Conecte seus prompts para criar uma sequência.", + "addConnection": "Adicionar Conexão", + "addPrevious": "Adicionar Anterior", + "addNext": "Adicionar Próximo", + "addConnectionTitle": "Adicionar Conexão", + "addPreviousTitle": "Adicionar Prompt Anterior", + "addNextTitle": "Adicionar Próximo Prompt", + "addConnectionDescription": "Vincule este prompt a outro prompt com um rótulo de condição.", + "addPreviousDescription": "Selecione um prompt que vem antes deste no fluxo de trabalho.", + "addNextDescription": "Selecione um prompt que vem depois deste no fluxo de trabalho.", + "noConnections": "Ainda não há prompts conectados. Adicione conexões para criar uma cadeia de prompts.", + "previousSteps": "Etapas anteriores", + "nextSteps": "Próximas etapas", + "searchPrompt": "Pesquisar Prompt", + "searchPlaceholder": "Pesquisar por título...", + "selectedPrompt": "Prompt Selecionado", + "connectionLabel": "Rótulo da Conexão", + "labelPlaceholder": "ex. primeiro quadro, próxima etapa, após processamento...", + "labelHint": "Descreva a condição ou transição entre prompts", + "change": "Alterar", + "cancel": "Cancelar", + "fillAllFields": "Por favor, selecione um prompt e insira um rótulo", + "connectionFailed": "Falha ao criar conexão", + "connectionAdded": "Conexão adicionada com sucesso", + "connectionDeleted": "Conexão excluída", + "deleteFailed": "Falha ao excluir conexão", + "noResults": "Nenhum prompt encontrado" + }, "promptWritingGuide": { "title": "Como Escrever Bons Prompts", "subtitle": "Dicas, exemplos e melhores práticas para criar prompts de IA eficazes", diff --git a/messages/ru.json b/messages/ru.json index c2a542b2..215418e2 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "При сохранении промптов через MCP делать их публичными по умолчанию вместо приватных.", "settingUpdated": "Настройка обновлена" }, + "connectedPrompts": { + "title": "Поток промптов", + "description": "Некоторые промпты работают как многоэтапные рабочие процессы. Соедините свои промпты для создания последовательности.", + "addConnection": "Добавить связь", + "addPrevious": "Добавить предыдущий", + "addNext": "Добавить следующий", + "addConnectionTitle": "Добавить связь", + "addPreviousTitle": "Добавить предыдущий промпт", + "addNextTitle": "Добавить следующий промпт", + "addConnectionDescription": "Свяжите этот промпт с другим промптом с меткой условия.", + "addPreviousDescription": "Выберите промпт, который идёт перед этим в рабочем процессе.", + "addNextDescription": "Выберите промпт, который идёт после этого в рабочем процессе.", + "noConnections": "Связанных промптов пока нет. Добавьте связи для создания цепочки промптов.", + "previousSteps": "Предыдущие шаги", + "nextSteps": "Следующие шаги", + "searchPrompt": "Поиск промпта", + "searchPlaceholder": "Поиск по названию...", + "selectedPrompt": "Выбранный промпт", + "connectionLabel": "Метка связи", + "labelPlaceholder": "напр. первый кадр, следующий шаг, после обработки...", + "labelHint": "Опишите условие или переход между промптами", + "change": "Изменить", + "cancel": "Отмена", + "fillAllFields": "Пожалуйста, выберите промпт и введите метку", + "connectionFailed": "Не удалось создать связь", + "connectionAdded": "Связь успешно добавлена", + "connectionDeleted": "Связь удалена", + "deleteFailed": "Не удалось удалить связь", + "noResults": "Промпты не найдены" + }, "promptWritingGuide": { "title": "Как писать отличные промпты", "subtitle": "Советы, примеры и лучшие практики для создания эффективных AI-промптов", diff --git a/messages/tr.json b/messages/tr.json index dbd76709..48fc7dd0 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "MCP ile prompt kaydederken, varsayılan olarak özel yerine herkese açık yap.", "settingUpdated": "Ayar güncellendi" }, + "connectedPrompts": { + "title": "Prompt Akışı", + "description": "Bazı promptlar çok adımlı iş akışları olarak çalışır. Bir sıra oluşturmak için promptlarınızı bağlayın.", + "addConnection": "Bağlantı Ekle", + "addPrevious": "Önceki Ekle", + "addNext": "Sonraki Ekle", + "addConnectionTitle": "Bağlantı Ekle", + "addPreviousTitle": "Önceki Prompt Ekle", + "addNextTitle": "Sonraki Prompt Ekle", + "addConnectionDescription": "Bu promptu bir koşul etiketi ile başka bir prompta bağlayın.", + "addPreviousDescription": "İş akışında bundan önce gelen bir prompt seçin.", + "addNextDescription": "İş akışında bundan sonra gelen bir prompt seçin.", + "noConnections": "Henüz bağlı prompt yok. Prompt zinciri oluşturmak için bağlantılar ekleyin.", + "previousSteps": "Önceki adımlar", + "nextSteps": "Sonraki adımlar", + "searchPrompt": "Prompt Ara", + "searchPlaceholder": "Başlığa göre ara...", + "selectedPrompt": "Seçili Prompt", + "connectionLabel": "Bağlantı Etiketi", + "labelPlaceholder": "örn. ilk kare, sonraki adım, işlemden sonra...", + "labelHint": "Promptlar arasındaki koşulu veya geçişi açıklayın", + "change": "Değiştir", + "cancel": "İptal", + "fillAllFields": "Lütfen bir prompt seçin ve etiket girin", + "connectionFailed": "Bağlantı oluşturulamadı", + "connectionAdded": "Bağlantı başarıyla eklendi", + "connectionDeleted": "Bağlantı silindi", + "deleteFailed": "Bağlantı silinemedi", + "noResults": "Prompt bulunamadı" + }, "promptWritingGuide": { "title": "Harika Promptlar Nasıl Yazılır", "subtitle": "Etkili AI promptları oluşturmak için ipuçları, örnekler ve en iyi uygulamalar", diff --git a/messages/zh.json b/messages/zh.json index 925a9907..e0244aa7 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -929,6 +929,36 @@ "publicByDefaultDescription": "通过 MCP 保存提示词时,默认设为公开而非私有。", "settingUpdated": "设置已更新" }, + "connectedPrompts": { + "title": "提示词流程", + "description": "某些提示词作为多步骤工作流运行。连接您的提示词以创建序列。", + "addConnection": "添加关联", + "addPrevious": "添加上一个", + "addNext": "添加下一个", + "addConnectionTitle": "添加关联", + "addPreviousTitle": "添加上一个提示词", + "addNextTitle": "添加下一个提示词", + "addConnectionDescription": "使用条件标签将此提示词链接到另一个提示词。", + "addPreviousDescription": "选择工作流中在此之前的提示词。", + "addNextDescription": "选择工作流中在此之后的提示词。", + "noConnections": "尚无关联提示词。添加关联以创建提示词链。", + "previousSteps": "上一步", + "nextSteps": "下一步", + "searchPrompt": "搜索提示词", + "searchPlaceholder": "按标题搜索...", + "selectedPrompt": "已选提示词", + "connectionLabel": "关联标签", + "labelPlaceholder": "例如:第一帧、下一步、处理后...", + "labelHint": "描述提示词之间的条件或转换", + "change": "更改", + "cancel": "取消", + "fillAllFields": "请选择提示词并输入标签", + "connectionFailed": "创建关联失败", + "connectionAdded": "关联添加成功", + "connectionDeleted": "关联已删除", + "deleteFailed": "删除关联失败", + "noResults": "未找到提示词" + }, "promptWritingGuide": { "title": "如何写出优秀的提示词", "subtitle": "创建有效 AI 提示词的技巧、示例和最佳实践", diff --git a/package-lock.json b/package-lock.json index 290ad6d1..ed08f97e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,10 +31,12 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@types/d3": "^7.4.3", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "d3": "^7.9.0", "date-fns": "^4.1.0", "lucide-react": "^0.556.0", "next": "^16.0.10", @@ -1041,7 +1043,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3179,7 +3180,6 @@ "integrity": "sha512-4SXj4Oo6HyQkLUWT8Ke5R0PTAfVOKip5Roo+6+b2EDTkFg5be0FnBWiuRJc0BC0sRQIWGMLKW1XguhVfW/z3/A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -5774,6 +5774,259 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5798,6 +6051,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -5851,7 +6110,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5862,7 +6120,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5872,7 +6129,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -5925,7 +6183,6 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -6444,7 +6701,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6768,7 +7024,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -6881,7 +7136,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7199,6 +7453,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7303,6 +7566,419 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7470,6 +8146,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7541,6 +8226,7 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7887,7 +8573,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8375,7 +9060,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9178,6 +9862,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/intl-messageformat": { "version": "10.7.18", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", @@ -10202,6 +10895,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10977,7 +11671,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.1.0.tgz", "integrity": "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", @@ -12144,7 +12837,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -12176,7 +12868,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.19.1", "@prisma/engines": "6.19.1" @@ -12356,7 +13047,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12366,7 +13056,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12379,7 +13068,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12655,6 +13343,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -12695,6 +13389,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -13437,7 +14137,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13672,7 +14371,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14166,7 +14864,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index cfbdfbb4..9486693a 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,12 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@types/d3": "^7.4.3", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "d3": "^7.9.0", "date-fns": "^4.1.0", "lucide-react": "^0.556.0", "next": "^16.0.10", diff --git a/prisma/migrations/20251221121143_add_prompt_connections/migration.sql b/prisma/migrations/20251221121143_add_prompt_connections/migration.sql new file mode 100644 index 00000000..cd232cb4 --- /dev/null +++ b/prisma/migrations/20251221121143_add_prompt_connections/migration.sql @@ -0,0 +1,30 @@ +-- DropIndex +DROP INDEX "users_githubUsername_idx"; + +-- CreateTable +CREATE TABLE "prompt_connections" ( + "id" TEXT NOT NULL, + "sourceId" TEXT NOT NULL, + "targetId" TEXT NOT NULL, + "label" TEXT NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "prompt_connections_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "prompt_connections_sourceId_idx" ON "prompt_connections"("sourceId"); + +-- CreateIndex +CREATE INDEX "prompt_connections_targetId_idx" ON "prompt_connections"("targetId"); + +-- CreateIndex +CREATE UNIQUE INDEX "prompt_connections_sourceId_targetId_key" ON "prompt_connections"("sourceId", "targetId"); + +-- AddForeignKey +ALTER TABLE "prompt_connections" ADD CONSTRAINT "prompt_connections_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "prompts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "prompt_connections" ADD CONSTRAINT "prompt_connections_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "prompts"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index afd73f1c..beb828c2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -114,6 +114,8 @@ model Prompt { author User @relation("PromptAuthor", fields: [authorId], references: [id], onDelete: Cascade) category Category? @relation(fields: [categoryId], references: [id]) contributors User[] @relation("PromptContributors") + outgoingConnections PromptConnection[] @relation("ConnectionSource") + incomingConnections PromptConnection[] @relation("ConnectionTarget") @@index([authorId]) @@index([categoryId]) @@ -327,6 +329,23 @@ model Notification { @@map("notifications") } +model PromptConnection { + id String @id @default(cuid()) + sourceId String + targetId String + label String + order Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + source Prompt @relation("ConnectionSource", fields: [sourceId], references: [id], onDelete: Cascade) + target Prompt @relation("ConnectionTarget", fields: [targetId], references: [id], onDelete: Cascade) + + @@unique([sourceId, targetId]) + @@index([sourceId]) + @@index([targetId]) + @@map("prompt_connections") +} + enum NotificationType { COMMENT REPLY diff --git a/prisma/seed.ts b/prisma/seed.ts index c2025e5f..8f597671 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -808,6 +808,113 @@ Provide specific, actionable feedback with: console.log("✅ Created", changeRequestsData.length, "change requests"); + // Create prompt connections (chains) + const connectionPromptTitles = [ + "Code Review Assistant", + "Code Refactoring Skill", + "Test Generator Skill", + "Documentation Writer Skill", + "Git Commit Message Skill", + "Code Review Skill", + "Debug Assistant", + "Content Pipeline Workflow", + "Blog Post Generator", + ]; + + const connectionPrompts = await prisma.prompt.findMany({ + where: { title: { in: connectionPromptTitles } }, + }); + + const getPromptByTitle = (title: string) => connectionPrompts.find(p => p.title === title); + + const promptConnectionsData = [ + // Code review workflow chain + { + sourceTitle: "Code Review Assistant", + targetTitle: "Code Refactoring Skill", + label: "needs refactoring", + order: 0, + }, + { + sourceTitle: "Code Review Assistant", + targetTitle: "Test Generator Skill", + label: "needs tests", + order: 1, + }, + { + sourceTitle: "Code Refactoring Skill", + targetTitle: "Code Review Skill", + label: "review changes", + order: 0, + }, + { + sourceTitle: "Code Refactoring Skill", + targetTitle: "Documentation Writer Skill", + label: "update docs", + order: 1, + }, + { + sourceTitle: "Test Generator Skill", + targetTitle: "Code Review Skill", + label: "review tests", + order: 0, + }, + // After code review, commit + { + sourceTitle: "Code Review Skill", + targetTitle: "Git Commit Message Skill", + label: "ready to commit", + order: 0, + }, + // Debug workflow + { + sourceTitle: "Debug Assistant", + targetTitle: "Code Refactoring Skill", + label: "fix identified", + order: 0, + }, + { + sourceTitle: "Debug Assistant", + targetTitle: "Test Generator Skill", + label: "add regression test", + order: 1, + }, + // Content workflow chain + { + sourceTitle: "Content Pipeline Workflow", + targetTitle: "Blog Post Generator", + label: "generate content", + order: 0, + }, + ]; + + let connectionsCreated = 0; + for (const conn of promptConnectionsData) { + const source = getPromptByTitle(conn.sourceTitle); + const target = getPromptByTitle(conn.targetTitle); + + if (source && target) { + await prisma.promptConnection.upsert({ + where: { + sourceId_targetId: { + sourceId: source.id, + targetId: target.id, + }, + }, + update: {}, + create: { + sourceId: source.id, + targetId: target.id, + label: conn.label, + order: conn.order, + }, + }); + connectionsCreated++; + } + } + + console.log("✅ Created", connectionsCreated, "prompt connections"); + console.log("\n🎉 Seeding complete!"); console.log("\n📋 Test credentials (all passwords: password123):"); console.log(" Admin: admin@prompts.chat"); diff --git a/src/app/api/prompts/[id]/connections/[connectionId]/route.ts b/src/app/api/prompts/[id]/connections/[connectionId]/route.ts new file mode 100644 index 00000000..0c7858e9 --- /dev/null +++ b/src/app/api/prompts/[id]/connections/[connectionId]/route.ts @@ -0,0 +1,141 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; + +const updateConnectionSchema = z.object({ + label: z.string().min(1).max(100).optional(), + order: z.number().int().min(0).optional(), +}); + +interface RouteParams { + params: Promise<{ id: string; connectionId: string }>; +} + +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id, connectionId } = await params; + + try { + const connection = await db.promptConnection.findUnique({ + where: { id: connectionId }, + include: { + source: { + select: { authorId: true }, + }, + }, + }); + + if (!connection) { + return NextResponse.json( + { error: "Connection not found" }, + { status: 404 } + ); + } + + if (connection.sourceId !== id) { + return NextResponse.json( + { error: "Connection does not belong to this prompt" }, + { status: 400 } + ); + } + + if ( + connection.source.authorId !== session.user.id && + session.user.role !== "ADMIN" + ) { + return NextResponse.json( + { error: "You can only delete connections from your own prompts" }, + { status: 403 } + ); + } + + await db.promptConnection.delete({ + where: { id: connectionId }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Failed to delete connection:", error); + return NextResponse.json( + { error: "Failed to delete connection" }, + { status: 500 } + ); + } +} + +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id, connectionId } = await params; + + try { + const body = await request.json(); + const data = updateConnectionSchema.parse(body); + + const connection = await db.promptConnection.findUnique({ + where: { id: connectionId }, + include: { + source: { + select: { authorId: true }, + }, + }, + }); + + if (!connection) { + return NextResponse.json( + { error: "Connection not found" }, + { status: 404 } + ); + } + + if (connection.sourceId !== id) { + return NextResponse.json( + { error: "Connection does not belong to this prompt" }, + { status: 400 } + ); + } + + if ( + connection.source.authorId !== session.user.id && + session.user.role !== "ADMIN" + ) { + return NextResponse.json( + { error: "You can only update connections on your own prompts" }, + { status: 403 } + ); + } + + const updated = await db.promptConnection.update({ + where: { id: connectionId }, + data, + include: { + target: { + select: { + id: true, + title: true, + slug: true, + }, + }, + }, + }); + + return NextResponse.json(updated); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues }, { status: 400 }); + } + console.error("Failed to update connection:", error); + return NextResponse.json( + { error: "Failed to update connection" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/prompts/[id]/connections/route.ts b/src/app/api/prompts/[id]/connections/route.ts new file mode 100644 index 00000000..9fa92b35 --- /dev/null +++ b/src/app/api/prompts/[id]/connections/route.ts @@ -0,0 +1,206 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; + +const createConnectionSchema = z.object({ + targetId: z.string().min(1), + label: z.string().min(1).max(100), + order: z.number().int().min(0).optional(), +}); + +interface RouteParams { + params: Promise<{ id: string }>; +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + const { id } = await params; + + try { + const prompt = await db.prompt.findUnique({ + where: { id, deletedAt: null }, + select: { id: true, isPrivate: true, authorId: true }, + }); + + if (!prompt) { + return NextResponse.json({ error: "Prompt not found" }, { status: 404 }); + } + + // Get all connections where this prompt is involved (source or target) + const outgoingConnections = await db.promptConnection.findMany({ + where: { sourceId: id }, + orderBy: { order: "asc" }, + include: { + target: { + select: { + id: true, + title: true, + slug: true, + isPrivate: true, + authorId: true, + }, + }, + }, + }); + + const incomingConnections = await db.promptConnection.findMany({ + where: { targetId: id }, + orderBy: { order: "asc" }, + include: { + source: { + select: { + id: true, + title: true, + slug: true, + isPrivate: true, + authorId: true, + }, + }, + }, + }); + + // Filter out private prompts the user can't see + const session = await auth(); + const userId = session?.user?.id; + + const filteredOutgoing = outgoingConnections.filter( + (c: typeof outgoingConnections[number]) => !c.target.isPrivate || c.target.authorId === userId + ); + + const filteredIncoming = incomingConnections.filter( + (c: typeof incomingConnections[number]) => !c.source.isPrivate || c.source.authorId === userId + ); + + return NextResponse.json({ + outgoing: filteredOutgoing, + incoming: filteredIncoming, + }); + } catch (error) { + console.error("Failed to fetch connections:", error); + return NextResponse.json( + { error: "Failed to fetch connections" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest, { params }: RouteParams) { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + + try { + const body = await request.json(); + const { targetId, label, order } = createConnectionSchema.parse(body); + + // Verify source prompt exists and user owns it + const sourcePrompt = await db.prompt.findUnique({ + where: { id, deletedAt: null }, + select: { authorId: true }, + }); + + if (!sourcePrompt) { + return NextResponse.json( + { error: "Source prompt not found" }, + { status: 404 } + ); + } + + if ( + sourcePrompt.authorId !== session.user.id && + session.user.role !== "ADMIN" + ) { + return NextResponse.json( + { error: "You can only add connections to your own prompts" }, + { status: 403 } + ); + } + + // Verify target prompt exists and belongs to the user + const targetPrompt = await db.prompt.findUnique({ + where: { id: targetId, deletedAt: null }, + select: { id: true, title: true, authorId: true }, + }); + + if (!targetPrompt) { + return NextResponse.json( + { error: "Target prompt not found" }, + { status: 404 } + ); + } + + // Verify user owns the target prompt (users can only connect their own prompts) + if ( + targetPrompt.authorId !== session.user.id && + session.user.role !== "ADMIN" + ) { + return NextResponse.json( + { error: "You can only connect to your own prompts" }, + { status: 403 } + ); + } + + // Prevent self-connection + if (id === targetId) { + return NextResponse.json( + { error: "Cannot connect a prompt to itself" }, + { status: 400 } + ); + } + + // Check if connection already exists + const existing = await db.promptConnection.findUnique({ + where: { sourceId_targetId: { sourceId: id, targetId } }, + }); + + if (existing) { + return NextResponse.json( + { error: "Connection already exists" }, + { status: 400 } + ); + } + + // Calculate order if not provided + let connectionOrder = order; + if (connectionOrder === undefined) { + const lastConnection = await db.promptConnection.findFirst({ + where: { sourceId: id }, + orderBy: { order: "desc" }, + select: { order: true }, + }); + connectionOrder = (lastConnection?.order ?? -1) + 1; + } + + const connection = await db.promptConnection.create({ + data: { + sourceId: id, + targetId, + label, + order: connectionOrder, + }, + include: { + target: { + select: { + id: true, + title: true, + slug: true, + }, + }, + }, + }); + + return NextResponse.json(connection, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues }, { status: 400 }); + } + console.error("Failed to create connection:", error); + return NextResponse.json( + { error: "Failed to create connection" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/prompts/search/route.ts b/src/app/api/prompts/search/route.ts new file mode 100644 index 00000000..7e317e43 --- /dev/null +++ b/src/app/api/prompts/search/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const query = searchParams.get("q") || ""; + const limit = Math.min(parseInt(searchParams.get("limit") || "10"), 50); + + if (query.length < 2) { + return NextResponse.json({ prompts: [] }); + } + + const session = await auth(); + + try { + const prompts = await db.prompt.findMany({ + where: { + deletedAt: null, + isUnlisted: false, + OR: [ + { isPrivate: false }, + ...(session?.user ? [{ authorId: session.user.id }] : []), + ], + title: { + contains: query, + mode: "insensitive", + }, + }, + select: { + id: true, + title: true, + slug: true, + author: { + select: { + username: true, + }, + }, + }, + take: limit, + orderBy: [ + { isFeatured: "desc" }, + { viewCount: "desc" }, + ], + }); + + return NextResponse.json({ prompts }); + } catch (error) { + console.error("Search failed:", error); + return NextResponse.json( + { error: "Search failed" }, + { status: 500 } + ); + } +} diff --git a/src/app/prompts/[id]/page.tsx b/src/app/prompts/[id]/page.tsx index affa9bfb..3529515b 100644 --- a/src/app/prompts/[id]/page.tsx +++ b/src/app/prompts/[id]/page.tsx @@ -24,6 +24,7 @@ import { MediaPreview } from "@/components/prompts/media-preview"; import { ReportPromptDialog } from "@/components/prompts/report-prompt-dialog"; import { DelistBanner } from "@/components/prompts/delist-banner"; import { CommentSection } from "@/components/comments"; +import { PromptConnections } from "@/components/prompts/prompt-connections"; import { getConfig } from "@/lib/config"; interface PromptPageProps { @@ -597,6 +598,13 @@ export default async function PromptPage({ params }: PromptPageProps) { )} + {/* Connected Prompts Section */} + + {/* Comments Section */} {config.features.comments !== false && !prompt.isPrivate && ( void; + promptId: string; + connectionType: "previous" | "next"; + onConnectionAdded: () => void; +} + +export function AddConnectionDialog({ + open, + onOpenChange, + promptId, + connectionType, + onConnectionAdded, +}: AddConnectionDialogProps) { + const t = useTranslations("connectedPrompts"); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [selectedPrompt, setSelectedPrompt] = useState(null); + const [label, setLabel] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(""); + + const debouncedQuery = useDebounce(searchQuery, 300); + + useEffect(() => { + if (!debouncedQuery || debouncedQuery.length < 2) { + setSearchResults([]); + return; + } + + const searchPrompts = async () => { + setIsSearching(true); + try { + const res = await fetch( + `/api/prompts/search?q=${encodeURIComponent(debouncedQuery)}&limit=10` + ); + if (res.ok) { + const data = await res.json(); + // Filter out the current prompt + setSearchResults( + data.prompts.filter((p: SearchResult) => p.id !== promptId) + ); + } + } catch (err) { + console.error("Search failed:", err); + } finally { + setIsSearching(false); + } + }; + + searchPrompts(); + }, [debouncedQuery, promptId]); + + const handleSubmit = async () => { + if (!selectedPrompt || !label.trim()) { + setError(t("fillAllFields")); + return; + } + + setIsSubmitting(true); + setError(""); + + try { + // For "previous" connections, we create the connection FROM the selected prompt TO the current prompt + // For "next" connections, we create the connection FROM the current prompt TO the selected prompt + const sourceId = connectionType === "previous" ? selectedPrompt.id : promptId; + const targetId = connectionType === "previous" ? promptId : selectedPrompt.id; + + const res = await fetch(`/api/prompts/${sourceId}/connections`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: targetId, + label: label.trim(), + }), + }); + + if (!res.ok) { + const data = await res.json(); + setError(data.error || t("connectionFailed")); + return; + } + + onConnectionAdded(); + handleClose(); + } catch { + setError(t("connectionFailed")); + } finally { + setIsSubmitting(false); + } + }; + + const handleClose = () => { + setSearchQuery(""); + setSearchResults([]); + setSelectedPrompt(null); + setLabel(""); + setError(""); + onOpenChange(false); + }; + + return ( + + + + + + {connectionType === "previous" ? t("addPreviousTitle") : t("addNextTitle")} + + + {connectionType === "previous" ? t("addPreviousDescription") : t("addNextDescription")} + + + +
+ {!selectedPrompt ? ( +
+ +
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ + {isSearching && ( +
+ +
+ )} + + {searchResults.length > 0 && ( +
+ {searchResults.map((prompt) => ( + + ))} +
+ )} + + {debouncedQuery.length >= 2 && + !isSearching && + searchResults.length === 0 && ( +

+ {t("noResults")} +

+ )} +
+ ) : ( +
+
+ +
+
+

{selectedPrompt.title}

+

+ @{selectedPrompt.author.username} +

+
+ +
+
+ +
+ + setLabel(e.target.value)} + className="mt-1.5" + maxLength={100} + /> +

+ {t("labelHint")} +

+
+
+ )} + + {error &&

{error}

} + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/prompts/prompt-connections.tsx b/src/components/prompts/prompt-connections.tsx new file mode 100644 index 00000000..5ec13a66 --- /dev/null +++ b/src/components/prompts/prompt-connections.tsx @@ -0,0 +1,608 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import * as d3 from "d3"; +import { Link2, ArrowUp, ArrowDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AddConnectionDialog } from "./add-connection-dialog"; + +interface PromptNode { + id: string; + title: string; + slug: string | null; +} + +interface OutgoingConnection { + id: string; + sourceId: string; + targetId: string; + label: string; + order: number; + target: PromptNode; +} + +interface IncomingConnection { + id: string; + sourceId: string; + targetId: string; + label: string; + order: number; + source: PromptNode; +} + +interface PromptConnectionsProps { + promptId: string; + promptTitle: string; + canEdit: boolean; +} + +interface GraphNode { + id: string; + title: string; + slug: string | null; + x: number; + y: number; + type: "current" | "incoming" | "outgoing"; +} + +interface GraphLink { + source: GraphNode; + target: GraphNode; + label: string; + connectionId: string; +} + +export function PromptConnections({ + promptId, + promptTitle, + canEdit, +}: PromptConnectionsProps) { + const t = useTranslations("connectedPrompts"); + const router = useRouter(); + const svgRef = useRef(null); + const [outgoing, setOutgoing] = useState([]); + const [incoming, setIncoming] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [connectionType, setConnectionType] = useState<"previous" | "next">("next"); + + const fetchConnections = useCallback(async () => { + try { + const res = await fetch(`/api/prompts/${promptId}/connections`); + if (res.ok) { + const data = await res.json(); + setOutgoing(data.outgoing || []); + setIncoming(data.incoming || []); + } + } catch (err) { + console.error("Failed to fetch connections:", err); + } finally { + setIsLoading(false); + } + }, [promptId]); + + useEffect(() => { + fetchConnections(); + }, [fetchConnections]); + + const handleDeleteConnection = useCallback(async (connectionId: string) => { + try { + const res = await fetch( + `/api/prompts/${promptId}/connections/${connectionId}`, + { method: "DELETE" } + ); + + if (res.ok) { + toast.success(t("connectionDeleted")); + fetchConnections(); + } else { + const data = await res.json(); + toast.error(data.error || t("deleteFailed")); + } + } catch { + toast.error(t("deleteFailed")); + } + }, [promptId, t, fetchConnections]); + + const handleConnectionAdded = () => { + toast.success(t("connectionAdded")); + fetchConnections(); + }; + + const getPromptUrl = (prompt: { slug: string | null; id: string }) => { + return prompt.slug ? `/prompts/${prompt.slug}` : `/prompts/${prompt.id}`; + }; + + // D3 Spider Visualization + useEffect(() => { + if (isLoading || !svgRef.current) return; + if (outgoing.length === 0 && incoming.length === 0) return; + + const renderGraph = () => { + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + // Get container dimensions + const container = svgRef.current?.parentElement; + const width = container?.clientWidth || 600; + + // Responsive sizing based on container width + const isMobile = width < 500; + const isTablet = width < 700; + + const nodeWidth = isMobile ? width - 40 : isTablet ? Math.min(160, width * 0.35) : Math.min(180, width * 0.3); + const nodeHeight = isMobile ? 36 : 40; + const verticalGap = isMobile ? 50 : 100; + const horizontalGap = isMobile ? 20 : isTablet ? 40 : 60; + const fontSize = isMobile ? "10px" : "11px"; + const labelFontSize = isMobile ? "8px" : "9px"; + + // Calculate layout dimensions + const incomingCount = incoming.length; + const outgoingCount = outgoing.length; + + // On mobile, stack nodes vertically if more than 1 + const stackIncoming = isMobile && incomingCount > 1; + const stackOutgoing = isMobile && outgoingCount > 1; + + // Calculate total height (add extra space for badges on mobile) + const badgeSpace = isMobile ? 24 : 0; + const incomingRows = stackIncoming ? incomingCount : 1; + const outgoingRows = stackOutgoing ? outgoingCount : 1; + const topRowHeight = incomingCount > 0 ? (nodeHeight + badgeSpace + 20) * incomingRows : 0; + const bottomRowHeight = outgoingCount > 0 ? (nodeHeight + badgeSpace + 20) * outgoingRows : 0; + const height = topRowHeight + nodeHeight + bottomRowHeight + verticalGap * 2 + 60; + + // Update SVG dimensions + svg.attr("width", width).attr("height", height); + + const centerX = width / 2; + const centerY = topRowHeight + verticalGap + nodeHeight / 2 + 20; + + // Create nodes + const nodes: GraphNode[] = []; + const links: GraphLink[] = []; + + // Current prompt (center) + const currentNode: GraphNode = { + id: promptId, + title: promptTitle, + slug: null, + x: centerX, + y: centerY, + type: "current", + }; + nodes.push(currentNode); + + // Incoming nodes (top) - stack on mobile, horizontal otherwise + if (stackIncoming) { + incoming.forEach((conn, i) => { + const node: GraphNode = { + id: conn.source.id, + title: conn.source.title, + slug: conn.source.slug, + x: centerX, + y: 20 + nodeHeight / 2 + i * (nodeHeight + 20), + type: "incoming", + }; + nodes.push(node); + links.push({ + source: node, + target: currentNode, + label: conn.label, + connectionId: conn.id, + }); + }); + } else { + const incomingRowWidth = incomingCount * nodeWidth + (incomingCount - 1) * horizontalGap; + const incomingStartX = centerX - incomingRowWidth / 2 + nodeWidth / 2; + const incomingY = 20 + nodeHeight / 2; + incoming.forEach((conn, i) => { + const node: GraphNode = { + id: conn.source.id, + title: conn.source.title, + slug: conn.source.slug, + x: incomingStartX + i * (nodeWidth + horizontalGap), + y: incomingY, + type: "incoming", + }; + nodes.push(node); + links.push({ + source: node, + target: currentNode, + label: conn.label, + connectionId: conn.id, + }); + }); + } + + // Outgoing nodes (bottom) - stack on mobile, horizontal otherwise + if (stackOutgoing) { + outgoing.forEach((conn, i) => { + const node: GraphNode = { + id: conn.target.id, + title: conn.target.title, + slug: conn.target.slug, + x: centerX, + y: centerY + verticalGap + nodeHeight / 2 + i * (nodeHeight + 20), + type: "outgoing", + }; + nodes.push(node); + links.push({ + source: currentNode, + target: node, + label: conn.label, + connectionId: conn.id, + }); + }); + } else { + const outgoingRowWidth = outgoingCount * nodeWidth + (outgoingCount - 1) * horizontalGap; + const outgoingStartX = centerX - outgoingRowWidth / 2 + nodeWidth / 2; + const outgoingY = centerY + verticalGap + nodeHeight / 2; + outgoing.forEach((conn, i) => { + const node: GraphNode = { + id: conn.target.id, + title: conn.target.title, + slug: conn.target.slug, + x: outgoingStartX + i * (nodeWidth + horizontalGap), + y: outgoingY, + type: "outgoing", + }; + nodes.push(node); + links.push({ + source: currentNode, + target: node, + label: conn.label, + connectionId: conn.id, + }); + }); + } + + // Check if dark mode + const isDark = document.documentElement.classList.contains("dark"); + + // Theme colors (light/dark) + const colors = { + primary: isDark ? "#f4f4f5" : "#18181b", + primaryFg: isDark ? "#18181b" : "#fafafa", + card: isDark ? "#27272a" : "#ffffff", + cardFg: isDark ? "#fafafa" : "#09090b", + border: isDark ? "#3f3f46" : "#e4e4e7", + muted: isDark ? "#27272a" : "#f4f4f5", + mutedFg: isDark ? "#a1a1aa" : "#71717a", + destructive: "#ef4444", + }; + + // Create gradient definitions + const defs = svg.append("defs"); + + // Arrow marker + defs + .append("marker") + .attr("id", "arrowhead") + .attr("viewBox", "-0 -5 10 10") + .attr("refX", 8) + .attr("refY", 0) + .attr("orient", "auto") + .attr("markerWidth", 8) + .attr("markerHeight", 8) + .append("path") + .attr("d", "M 0,-4 L 8,0 L 0,4") + .attr("fill", colors.mutedFg); + + // Drop shadow filter + const filter = defs + .append("filter") + .attr("id", "shadow") + .attr("x", "-20%") + .attr("y", "-20%") + .attr("width", "140%") + .attr("height", "140%"); + filter + .append("feDropShadow") + .attr("dx", 0) + .attr("dy", 2) + .attr("stdDeviation", 3) + .attr("flood-opacity", 0.15); + + // Draw links (curved paths - vertical) + const linkGroup = svg.append("g").attr("class", "links"); + + linkGroup + .selectAll("path") + .data(links) + .join("path") + .attr("d", (d) => { + // For vertical layout: curves go from top to bottom + const sourceY = d.source.y + (d.source.type === "current" ? -nodeHeight / 2 : nodeHeight / 2); + const targetY = d.target.y + (d.target.type === "current" ? -nodeHeight / 2 : nodeHeight / 2); + const midY = (sourceY + targetY) / 2; + return `M ${d.source.x} ${sourceY} C ${d.source.x} ${midY}, ${d.target.x} ${midY}, ${d.target.x} ${targetY}`; + }) + .attr("fill", "none") + .attr("stroke", `${colors.mutedFg}50`) + .attr("stroke-width", 2) + .attr("marker-end", "url(#arrowhead)"); + + // Draw link labels with background (only on desktop) + if (!isMobile) { + const labelGroup = svg.append("g").attr("class", "labels"); + + const labelElements = labelGroup + .selectAll("g") + .data(links) + .join("g") + .attr("transform", (d) => { + const x = (d.source.x + d.target.x) / 2; + const y = (d.source.y + d.target.y) / 2; + return `translate(${x}, ${y})`; + }); + + labelElements + .append("rect") + .attr("x", (d) => -(d.label.length * 2.5 + 8)) + .attr("y", -8) + .attr("width", (d) => d.label.length * 5 + 16) + .attr("height", 16) + .attr("rx", 8) + .attr("fill", colors.muted) + .attr("stroke", colors.border) + .attr("stroke-width", 1); + + labelElements + .append("text") + .attr("text-anchor", "middle") + .attr("dy", "0.35em") + .attr("fill", colors.mutedFg) + .attr("font-size", labelFontSize) + .attr("font-weight", "500") + .text((d) => d.label); + } + + // Draw nodes + const nodeGroup = svg.append("g").attr("class", "nodes"); + + const nodeElements = nodeGroup + .selectAll("g") + .data(nodes) + .join("g") + .attr("transform", (d) => `translate(${d.x}, ${d.y})`) + .attr("cursor", (d) => (d.type !== "current" ? "pointer" : "default")) + .on("click", (event, d) => { + if (d.type !== "current") { + router.push(getPromptUrl(d)); + } + }) + .on("mouseover", function (event, d) { + if (d.type !== "current") { + d3.select(this).select("rect").attr("filter", "url(#shadow)"); + } + }) + .on("mouseout", function (event, d) { + if (d.type !== "current") { + d3.select(this).select("rect").attr("filter", null); + } + }); + + // Node backgrounds (rounded rectangles with better styling) + nodeElements + .append("rect") + .attr("x", -nodeWidth / 2) + .attr("y", -nodeHeight / 2) + .attr("width", nodeWidth) + .attr("height", nodeHeight) + .attr("rx", 10) + .attr("fill", (d) => + d.type === "current" ? colors.primary : colors.card + ) + .attr("stroke", (d) => + d.type === "current" ? colors.primary : colors.border + ) + .attr("stroke-width", (d) => (d.type === "current" ? 0 : 1.5)) + .attr("filter", (d) => (d.type === "current" ? "url(#shadow)" : null)); + + // Node labels + nodeElements + .append("text") + .attr("text-anchor", "middle") + .attr("dy", "0.35em") + .attr("fill", (d) => + d.type === "current" ? colors.primaryFg : colors.cardFg + ) + .attr("font-size", fontSize) + .attr("font-weight", (d) => (d.type === "current" ? "700" : "500")) + .text((d) => d.title); + + // On mobile, add badge labels below each non-current node + if (isMobile) { + nodeElements + .filter((d) => d.type !== "current") + .each(function(d) { + const link = links.find(l => + (d.type === "incoming" && l.source.id === d.id) || + (d.type === "outgoing" && l.target.id === d.id) + ); + if (link) { + const badgeLabel = link.label; + const badgeWidth = badgeLabel.length * 5 + 12; + const g = d3.select(this); + + // Badge background (above the node) + g.append("rect") + .attr("x", -badgeWidth / 2) + .attr("y", -nodeHeight / 2 - 20) + .attr("width", badgeWidth) + .attr("height", 16) + .attr("rx", 8) + .attr("fill", colors.muted) + .attr("stroke", colors.border) + .attr("stroke-width", 1); + + // Badge text (above the node) + g.append("text") + .attr("text-anchor", "middle") + .attr("y", -nodeHeight / 2 - 12) + .attr("dy", "0.35em") + .attr("fill", colors.mutedFg) + .attr("font-size", "8px") + .attr("font-weight", "500") + .text(badgeLabel); + } + }); + } else { + // Add type indicator for non-current nodes (desktop only) + nodeElements + .filter((d) => d.type !== "current") + .append("text") + .attr("text-anchor", "middle") + .attr("dy", (d) => d.type === "incoming" ? nodeHeight / 2 + 14 : -nodeHeight / 2 - 6) + .attr("fill", colors.mutedFg) + .attr("font-size", "10px") + .text((d) => (d.type === "incoming" ? "↓" : "↓")); + } + + // Add delete buttons for outgoing connections (owner only) + if (canEdit) { + const outgoingNodes = nodeElements.filter((d) => d.type === "outgoing"); + + outgoingNodes + .append("circle") + .attr("cx", nodeWidth / 2 - 8) + .attr("cy", -nodeHeight / 2 + 8) + .attr("r", 10) + .attr("fill", colors.destructive) + .attr("cursor", "pointer") + .attr("opacity", 0) + .attr("class", "delete-btn") + .on("click", (event, d) => { + event.stopPropagation(); + const link = links.find( + (l) => l.target.id === d.id && l.source.id === promptId + ); + if (link) { + handleDeleteConnection(link.connectionId); + } + }); + + outgoingNodes + .append("text") + .attr("x", nodeWidth / 2 - 8) + .attr("y", -nodeHeight / 2 + 12) + .attr("text-anchor", "middle") + .attr("fill", "white") + .attr("font-size", "14px") + .attr("font-weight", "bold") + .attr("pointer-events", "none") + .attr("opacity", 0) + .attr("class", "delete-icon") + .text("×"); + + // Show delete button on hover + outgoingNodes + .on("mouseover", function () { + d3.select(this).select(".delete-btn").attr("opacity", 1); + d3.select(this).select(".delete-icon").attr("opacity", 1); + d3.select(this).select("rect").attr("filter", "url(#shadow)"); + }) + .on("mouseout", function () { + d3.select(this).select(".delete-btn").attr("opacity", 0); + d3.select(this).select(".delete-icon").attr("opacity", 0); + d3.select(this).select("rect").attr("filter", null); + }); + } + }; + + // Initial render + renderGraph(); + + // Add resize listener for responsiveness + const handleResize = () => { + renderGraph(); + }; + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isLoading, incoming, outgoing, promptId, promptTitle, canEdit, router, handleDeleteConnection]); + + if (isLoading) { + return null; + } + + // Don't render if no connections and user can't edit + if (!canEdit && outgoing.length === 0 && incoming.length === 0) { + return null; + } + + return ( +
+
+
+
+ +

{t("title")}

+
+

{t("description")}

+
+ {canEdit && ( +
+ + +
+ )} +
+ + {outgoing.length === 0 && incoming.length === 0 ? ( +

{t("noConnections")}

+ ) : ( +
+ {/* Vertical flow labels */} + {incoming.length > 0 && ( +

{t("previousSteps")}

+ )} + {/* D3 Vertical Graph */} +
+ +
+ {outgoing.length > 0 && ( +

{t("nextSteps")}

+ )} +
+ )} + + {canEdit && ( + + )} +
+ ); +} diff --git a/src/lib/hooks/use-debounce.ts b/src/lib/hooks/use-debounce.ts new file mode 100644 index 00000000..ba49720b --- /dev/null +++ b/src/lib/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}