feat(messages): add new translations for 'workflows' in multiple languages
This commit is contained in:
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "الأوامر",
|
||||
"skills": "المهارات",
|
||||
"workflows": "سير العمل",
|
||||
"categories": "الفئات",
|
||||
"tags": "الوسوم",
|
||||
"settings": "الإعدادات",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "المطورون",
|
||||
"book": "كتاب",
|
||||
"forKids": "للأطفال"
|
||||
"forKids": "للأطفال",
|
||||
"more": "المزيد"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "نسخ شعار SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "اشترك في التصنيفات لرؤية الأوامر هنا",
|
||||
"viewAllCategories": "عرض جميع التصنيفات"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "سير العمل",
|
||||
"description": "أوامر ذات تدفقات واتصالات متسلسلة",
|
||||
"noWorkflows": "لا توجد سير عمل بعد",
|
||||
"noWorkflowsDescription": "سير العمل هي أوامر تتصل بأوامر أخرى بالتسلسل. أنشئ أمرًا وأضف اتصالات لبناء سير عمل.",
|
||||
"browsePrompts": "تصفح الأوامر"
|
||||
},
|
||||
"collection": {
|
||||
"title": "مجموعتي",
|
||||
"description": "الأوامر التي حفظتها لوقت لاحق",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "لا توجد أوامر متصلة بعد. أضف اتصالات لإنشاء سلسلة أوامر.",
|
||||
"previousSteps": "الخطوات السابقة",
|
||||
"nextSteps": "الخطوات التالية",
|
||||
"fullFlow": "سير العمل الكامل",
|
||||
"searchPrompt": "بحث عن أمر",
|
||||
"searchPlaceholder": "البحث بالعنوان...",
|
||||
"selectedPrompt": "الأمر المحدد",
|
||||
|
||||
+12
-2
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasterlər",
|
||||
"prompts": "Promptlar",
|
||||
"skills": "Bacarıqlar",
|
||||
"workflows": "İş axınları",
|
||||
"categories": "Kateqoriyalar",
|
||||
"tags": "Etiketlər",
|
||||
"settings": "Parametrlər",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Tərtibatçılar",
|
||||
"book": "Kitab",
|
||||
"forKids": "Uşaqlar Üçün"
|
||||
"forKids": "Uşaqlar Üçün",
|
||||
"more": "Daha çox"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Logo SVG-ni Kopyala",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Burada promptları görmək üçün kateqoriyalara abunə olun",
|
||||
"viewAllCategories": "Bütün Kateqoriyalara Bax"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "İş axınları",
|
||||
"description": "Ardıcıl axınları və əlaqələri olan promptlar",
|
||||
"noWorkflows": "Hələ iş axını yoxdur",
|
||||
"noWorkflowsDescription": "İş axınları digər promptlara ardıcıl olaraq bağlanan promptlardır. İş axını yaratmaq üçün prompt yaradın və əlaqələr əlavə edin.",
|
||||
"browsePrompts": "Promptlara Bax"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Kolleksiyam",
|
||||
"description": "Sonra üçün saxladığınız promptlar",
|
||||
@@ -1197,7 +1206,8 @@
|
||||
"addNextDescription": "İş axınında bundan sonra gələn promptu seçin.",
|
||||
"noConnections": "Hələ bağlı prompt yoxdur. Prompt zənciri yaratmaq üçün əlaqələr əlavə edin.",
|
||||
"previousSteps": "Əvvəlki addımlar",
|
||||
"nextSteps": "Növbəti addımlar",
|
||||
"nextSteps": "Sonrakı addımlar",
|
||||
"fullFlow": "Tam Axın",
|
||||
"searchPrompt": "Prompt Axtar",
|
||||
"searchPlaceholder": "Başlığa görə axtar...",
|
||||
"selectedPrompt": "Seçilmiş Prompt",
|
||||
|
||||
+12
-2
@@ -38,7 +38,8 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Skills",
|
||||
"skills": "Fähigkeiten",
|
||||
"workflows": "Workflows",
|
||||
"categories": "Kategorien",
|
||||
"tags": "Tags",
|
||||
"settings": "Einstellungen",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Entwickler",
|
||||
"book": "Buch",
|
||||
"forKids": "Für Kinder"
|
||||
"forKids": "Für Kinder",
|
||||
"more": "Mehr"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Logo SVG kopieren",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Abonnieren Sie Kategorien, um hier Prompts zu sehen",
|
||||
"viewAllCategories": "Alle Kategorien anzeigen"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Workflows",
|
||||
"description": "Prompts mit sequentiellen Abläufen und Verbindungen",
|
||||
"noWorkflows": "Noch keine Workflows",
|
||||
"noWorkflowsDescription": "Workflows sind Prompts, die in einer Sequenz mit anderen Prompts verbunden sind. Erstellen Sie einen Prompt und fügen Sie Verbindungen hinzu, um einen Workflow zu erstellen.",
|
||||
"browsePrompts": "Prompts durchsuchen"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Meine Sammlung",
|
||||
"description": "Prompts, die Sie für später gespeichert haben",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "Noch keine verbundenen Prompts. Fügen Sie Verbindungen hinzu, um eine Prompt-Kette zu erstellen.",
|
||||
"previousSteps": "Vorherige Schritte",
|
||||
"nextSteps": "Nächste Schritte",
|
||||
"fullFlow": "Vollständiger Workflow",
|
||||
"searchPrompt": "Prompt suchen",
|
||||
"searchPlaceholder": "Nach Titel suchen...",
|
||||
"selectedPrompt": "Ausgewählter Prompt",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Δεξιότητες",
|
||||
"workflows": "Ροές εργασίας",
|
||||
"categories": "Κατηγορίες",
|
||||
"tags": "Ετικέτες",
|
||||
"settings": "Ρυθμίσεις",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Προγραμματιστές",
|
||||
"book": "Βιβλίο",
|
||||
"forKids": "Για Παιδιά"
|
||||
"forKids": "Για Παιδιά",
|
||||
"more": "Περισσότερα"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Αντιγραφή Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Εγγραφείτε σε κατηγορίες για να δείτε prompts εδώ",
|
||||
"viewAllCategories": "Προβολή όλων των κατηγοριών"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Ροές εργασίας",
|
||||
"description": "Prompts με διαδοχικές ροές και συνδέσεις",
|
||||
"noWorkflows": "Δεν υπάρχουν ακόμα ροές εργασίας",
|
||||
"noWorkflowsDescription": "Οι ροές εργασίας είναι prompts που συνδέονται με άλλα prompts σε ακολουθία. Δημιουργήστε ένα prompt και προσθέστε συνδέσεις για να δημιουργήσετε μια ροή εργασίας.",
|
||||
"browsePrompts": "Περιήγηση Prompts"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Η Συλλογή μου",
|
||||
"description": "Prompts που αποθηκεύσατε για αργότερα",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "Δεν υπάρχουν συνδεδεμένα prompts ακόμα. Προσθέστε συνδέσεις για να δημιουργήσετε μια αλυσίδα prompts.",
|
||||
"previousSteps": "Προηγούμενα βήματα",
|
||||
"nextSteps": "Επόμενα βήματα",
|
||||
"fullFlow": "Πλήρης ροή εργασίας",
|
||||
"searchPrompt": "Αναζήτηση Prompt",
|
||||
"searchPlaceholder": "Αναζήτηση με τίτλο...",
|
||||
"selectedPrompt": "Επιλεγμένο Prompt",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Skills",
|
||||
"workflows": "Workflows",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"settings": "Settings",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Developers",
|
||||
"book": "Book",
|
||||
"forKids": "For Kids"
|
||||
"forKids": "For Kids",
|
||||
"more": "More"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Copy Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Subscribe to categories to see prompts here",
|
||||
"viewAllCategories": "View All Categories"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Workflows",
|
||||
"description": "Prompts with sequential flows and connections",
|
||||
"noWorkflows": "No workflows yet",
|
||||
"noWorkflowsDescription": "Workflows are prompts that connect to other prompts in a sequence. Create a prompt and add connections to build a workflow.",
|
||||
"browsePrompts": "Browse Prompts"
|
||||
},
|
||||
"collection": {
|
||||
"title": "My Collection",
|
||||
"description": "Prompts you've saved for later",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "No connected prompts yet. Add connections to create a prompt chain.",
|
||||
"previousSteps": "Previous steps",
|
||||
"nextSteps": "Next steps",
|
||||
"fullFlow": "Complete Workflow",
|
||||
"searchPrompt": "Search Prompt",
|
||||
"searchPlaceholder": "Search by title...",
|
||||
"selectedPrompt": "Selected Prompt",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Habilidades",
|
||||
"workflows": "Flujos de trabajo",
|
||||
"categories": "Categorías",
|
||||
"tags": "Etiquetas",
|
||||
"settings": "Configuración",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Desarrolladores",
|
||||
"book": "Libro",
|
||||
"forKids": "Para Niños"
|
||||
"forKids": "Para Niños",
|
||||
"more": "Más"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Copiar Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Suscríbete a categorías para ver prompts aquí",
|
||||
"viewAllCategories": "Ver Todas las Categorías"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Flujos de trabajo",
|
||||
"description": "Prompts con flujos secuenciales y conexiones",
|
||||
"noWorkflows": "Aún no hay flujos de trabajo",
|
||||
"noWorkflowsDescription": "Los flujos de trabajo son prompts que se conectan a otros prompts en secuencia. Crea un prompt y agrega conexiones para construir un flujo de trabajo.",
|
||||
"browsePrompts": "Explorar Prompts"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Mi Colección",
|
||||
"description": "Prompts que has guardado para después",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "Aún no hay prompts conectados. Añade conexiones para crear una cadena de prompts.",
|
||||
"previousSteps": "Pasos anteriores",
|
||||
"nextSteps": "Siguientes pasos",
|
||||
"fullFlow": "Flujo completo",
|
||||
"searchPrompt": "Buscar Prompt",
|
||||
"searchPlaceholder": "Buscar por título...",
|
||||
"selectedPrompt": "Prompt Seleccionado",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "پرامپتمسترها",
|
||||
"prompts": "پرامپتها",
|
||||
"skills": "مهارتها",
|
||||
"workflows": "گردش کار",
|
||||
"categories": "دستهبندیها",
|
||||
"tags": "برچسبها",
|
||||
"settings": "تنظیمات",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "توسعهدهندگان",
|
||||
"book": "کتاب",
|
||||
"forKids": "برای کودکان"
|
||||
"forKids": "برای کودکان",
|
||||
"more": "بیشتر"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "کپی SVG لوگو",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "برای دیدن پرامپتها، دستهبندیها را دنبال کنید",
|
||||
"viewAllCategories": "مشاهده همه دستهبندیها"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "گردش کار",
|
||||
"description": "پرامپتها با جریانها و اتصالات ترتیبی",
|
||||
"noWorkflows": "هنوز گردش کاری وجود ندارد",
|
||||
"noWorkflowsDescription": "گردش کار پرامپتهایی هستند که به ترتیب به پرامپتهای دیگر متصل میشوند. یک پرامپت ایجاد کنید و اتصالات اضافه کنید تا گردش کار بسازید.",
|
||||
"browsePrompts": "مرور پرامپتها"
|
||||
},
|
||||
"collection": {
|
||||
"title": "مجموعه من",
|
||||
"description": "پرامپتهایی که برای بعد ذخیره کردید",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "هنوز پرامپت متصلی نیست. اتصالاتی اضافه کنید تا یک زنجیره پرامپت بسازید.",
|
||||
"previousSteps": "مراحل قبلی",
|
||||
"nextSteps": "مراحل بعدی",
|
||||
"fullFlow": "گردش کار کامل",
|
||||
"searchPrompt": "جستجوی پرامپت",
|
||||
"searchPlaceholder": "جستجو بر اساس عنوان...",
|
||||
"selectedPrompt": "پرامپت انتخاب شده",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Compétences",
|
||||
"workflows": "Flux de travail",
|
||||
"categories": "Catégories",
|
||||
"tags": "Tags",
|
||||
"settings": "Paramètres",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Développeurs",
|
||||
"book": "Livre",
|
||||
"forKids": "Pour Enfants"
|
||||
"forKids": "Pour Enfants",
|
||||
"more": "Plus"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Copier le logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Abonnez-vous à des catégories pour voir des prompts ici",
|
||||
"viewAllCategories": "Voir Toutes les Catégories"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Flux de travail",
|
||||
"description": "Prompts avec des flux et connexions séquentiels",
|
||||
"noWorkflows": "Pas encore de flux de travail",
|
||||
"noWorkflowsDescription": "Les flux de travail sont des prompts qui se connectent à d'autres prompts en séquence. Créez un prompt et ajoutez des connexions pour construire un flux de travail.",
|
||||
"browsePrompts": "Parcourir les Prompts"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Ma Collection",
|
||||
"description": "Prompts que vous avez sauvegardés pour plus tard",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"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",
|
||||
"fullFlow": "Flux complet",
|
||||
"searchPrompt": "Rechercher un Prompt",
|
||||
"searchPlaceholder": "Rechercher par titre...",
|
||||
"selectedPrompt": "Prompt Sélectionné",
|
||||
|
||||
+13
-3
@@ -38,7 +38,8 @@
|
||||
"feed": "פיד",
|
||||
"promptmasters": "מאסטרים",
|
||||
"prompts": "פרומפטים",
|
||||
"skills": "מיומנויות",
|
||||
"skills": "כישורים",
|
||||
"workflows": "תהליכי עבודה",
|
||||
"categories": "קטגוריות",
|
||||
"tags": "תגיות",
|
||||
"settings": "הגדרות",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "מפתחים",
|
||||
"book": "ספר",
|
||||
"forKids": "לילדים"
|
||||
"forKids": "לילדים",
|
||||
"more": "עוד"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "העתק לוגו SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "הירשם לקטגוריות כדי לראות פרומפטים כאן",
|
||||
"viewAllCategories": "צפה בכל הקטגוריות"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "תהליכי עבודה",
|
||||
"description": "פרומפטים עם זרימות וחיבורים רציפים",
|
||||
"noWorkflows": "עדיין אין תהליכי עבודה",
|
||||
"noWorkflowsDescription": "תהליכי עבודה הם פרומפטים שמתחברים לפרומפטים אחרים ברצף. צור פרומפט והוסף חיבורים כדי לבנות תהליך עבודה.",
|
||||
"browsePrompts": "עיין בפרומפטים"
|
||||
},
|
||||
"collection": {
|
||||
"title": "האוסף שלי",
|
||||
"description": "פרומפטים ששמרת למועד מאוחר יותר",
|
||||
@@ -1197,7 +1206,8 @@
|
||||
"addNextDescription": "בחר פרומפט שבא אחרי זה בתהליך העבודה.",
|
||||
"noConnections": "אין עדיין פרומפטים מקושרים. הוסף קישורים ליצירת שרשרת פרומפטים.",
|
||||
"previousSteps": "שלבים קודמים",
|
||||
"nextSteps": "שלבים הבאים",
|
||||
"nextSteps": "השלבים הבאים",
|
||||
"fullFlow": "תהליך עבודה מלא",
|
||||
"searchPrompt": "חפש פרומפט",
|
||||
"searchPlaceholder": "חפש לפי כותרת...",
|
||||
"selectedPrompt": "פרומפט נבחר",
|
||||
|
||||
+12
-2
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmaster",
|
||||
"prompts": "Prompt",
|
||||
"skills": "Competenze",
|
||||
"workflows": "Flussi di lavoro",
|
||||
"categories": "Categorie",
|
||||
"tags": "Tag",
|
||||
"settings": "Impostazioni",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Sviluppatori",
|
||||
"book": "Libro",
|
||||
"forKids": "Per Bambini"
|
||||
"forKids": "Per Bambini",
|
||||
"more": "Altro"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Copia Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Iscriviti alle categorie per vedere i prompt qui",
|
||||
"viewAllCategories": "Vedi Tutte le Categorie"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Flussi di lavoro",
|
||||
"description": "Prompt con flussi e connessioni sequenziali",
|
||||
"noWorkflows": "Ancora nessun flusso di lavoro",
|
||||
"noWorkflowsDescription": "I flussi di lavoro sono prompt che si connettono ad altri prompt in sequenza. Crea un prompt e aggiungi connessioni per costruire un flusso di lavoro.",
|
||||
"browsePrompts": "Sfoglia Prompt"
|
||||
},
|
||||
"collection": {
|
||||
"title": "La Mia Collezione",
|
||||
"description": "Prompt che hai salvato per dopo",
|
||||
@@ -1197,7 +1206,8 @@
|
||||
"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",
|
||||
"nextSteps": "Passi successivi",
|
||||
"fullFlow": "Flusso completo",
|
||||
"searchPrompt": "Cerca Prompt",
|
||||
"searchPlaceholder": "Cerca per titolo...",
|
||||
"selectedPrompt": "Prompt Selezionato",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "プロンプト",
|
||||
"skills": "スキル",
|
||||
"workflows": "ワークフロー",
|
||||
"categories": "カテゴリー",
|
||||
"tags": "タグ",
|
||||
"settings": "設定",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "開発者",
|
||||
"book": "本",
|
||||
"forKids": "子ども向け"
|
||||
"forKids": "子ども向け",
|
||||
"more": "もっと見る"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "ロゴSVGをコピー",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "カテゴリーを購読してプロンプトを表示",
|
||||
"viewAllCategories": "すべてのカテゴリーを見る"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "ワークフロー",
|
||||
"description": "シーケンシャルなフローと接続を持つプロンプト",
|
||||
"noWorkflows": "まだワークフローがありません",
|
||||
"noWorkflowsDescription": "ワークフローは、他のプロンプトに順番に接続するプロンプトです。プロンプトを作成し、接続を追加してワークフローを構築してください。",
|
||||
"browsePrompts": "プロンプトを見る"
|
||||
},
|
||||
"collection": {
|
||||
"title": "マイコレクション",
|
||||
"description": "保存したプロンプト",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "接続されたプロンプトはまだありません。プロンプトチェーンを作成するには接続を追加してください。",
|
||||
"previousSteps": "前のステップ",
|
||||
"nextSteps": "次のステップ",
|
||||
"fullFlow": "完全なフロー",
|
||||
"searchPrompt": "プロンプトを検索",
|
||||
"searchPlaceholder": "タイトルで検索...",
|
||||
"selectedPrompt": "選択したプロンプト",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "프롬프트마스터",
|
||||
"prompts": "프롬프트",
|
||||
"skills": "스킬",
|
||||
"workflows": "워크플로우",
|
||||
"categories": "카테고리",
|
||||
"tags": "태그",
|
||||
"settings": "설정",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "개발자",
|
||||
"book": "책",
|
||||
"forKids": "어린이용"
|
||||
"forKids": "어린이용",
|
||||
"more": "더 보기"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "로고 SVG 복사",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "카테고리를 구독하여 여기서 프롬프트를 확인하세요",
|
||||
"viewAllCategories": "모든 카테고리 보기"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "워크플로우",
|
||||
"description": "순차적 흐름과 연결이 있는 프롬프트",
|
||||
"noWorkflows": "아직 워크플로우가 없습니다",
|
||||
"noWorkflowsDescription": "워크플로우는 다른 프롬프트에 순차적으로 연결되는 프롬프트입니다. 프롬프트를 만들고 연결을 추가하여 워크플로우를 구축하세요.",
|
||||
"browsePrompts": "프롬프트 둘러보기"
|
||||
},
|
||||
"collection": {
|
||||
"title": "내 컬렉션",
|
||||
"description": "나중을 위해 저장한 프롬프트",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "아직 연결된 프롬프트가 없습니다. 프롬프트 체인을 만들려면 연결을 추가하세요.",
|
||||
"previousSteps": "이전 단계",
|
||||
"nextSteps": "다음 단계",
|
||||
"fullFlow": "전체 흐름",
|
||||
"searchPrompt": "프롬프트 검색",
|
||||
"searchPlaceholder": "제목으로 검색...",
|
||||
"selectedPrompt": "선택된 프롬프트",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Vaardigheden",
|
||||
"workflows": "Workflows",
|
||||
"categories": "Categorieën",
|
||||
"tags": "Tags",
|
||||
"settings": "Instellingen",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Ontwikkelaars",
|
||||
"book": "Boek",
|
||||
"forKids": "Voor kinderen"
|
||||
"forKids": "Voor kinderen",
|
||||
"more": "Meer"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Logo SVG kopiëren",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Abonneer op categorieën om hier prompts te zien",
|
||||
"viewAllCategories": "Alle categorieën bekijken"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Workflows",
|
||||
"description": "Prompts met sequentiële stromen en verbindingen",
|
||||
"noWorkflows": "Nog geen workflows",
|
||||
"noWorkflowsDescription": "Workflows zijn prompts die in volgorde verbonden zijn met andere prompts. Maak een prompt en voeg verbindingen toe om een workflow te bouwen.",
|
||||
"browsePrompts": "Prompts bekijken"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Mijn collectie",
|
||||
"description": "Prompts die je hebt opgeslagen voor later",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "Nog geen verbonden prompts. Voeg verbindingen toe om een promptketen te maken.",
|
||||
"previousSteps": "Vorige stappen",
|
||||
"nextSteps": "Volgende stappen",
|
||||
"fullFlow": "Volledige workflow",
|
||||
"searchPrompt": "Prompt zoeken",
|
||||
"searchPlaceholder": "Zoeken op titel...",
|
||||
"selectedPrompt": "Geselecteerde prompt",
|
||||
|
||||
+12
-2
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Habilidades",
|
||||
"workflows": "Fluxos de trabalho",
|
||||
"categories": "Categorias",
|
||||
"tags": "Tags",
|
||||
"settings": "Configurações",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Desenvolvedores",
|
||||
"book": "Livro",
|
||||
"forKids": "Para Crianças"
|
||||
"forKids": "Para Crianças",
|
||||
"more": "Mais"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Copiar Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Inscreva-se em categorias para ver prompts aqui",
|
||||
"viewAllCategories": "Ver Todas as Categorias"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Fluxos de trabalho",
|
||||
"description": "Prompts com fluxos e conexões sequenciais",
|
||||
"noWorkflows": "Ainda não há fluxos de trabalho",
|
||||
"noWorkflowsDescription": "Fluxos de trabalho são prompts que se conectam a outros prompts em sequência. Crie um prompt e adicione conexões para construir um fluxo de trabalho.",
|
||||
"browsePrompts": "Explorar Prompts"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Minha Coleção",
|
||||
"description": "Prompts que você salvou para depois",
|
||||
@@ -1197,7 +1206,8 @@
|
||||
"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",
|
||||
"nextSteps": "Próximos passos",
|
||||
"fullFlow": "Fluxo completo",
|
||||
"searchPrompt": "Pesquisar Prompt",
|
||||
"searchPlaceholder": "Pesquisar por título...",
|
||||
"selectedPrompt": "Prompt Selecionado",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Мастера промптов",
|
||||
"prompts": "Промпты",
|
||||
"skills": "Навыки",
|
||||
"workflows": "Рабочие процессы",
|
||||
"categories": "Категории",
|
||||
"tags": "Теги",
|
||||
"settings": "Настройки",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Разработчикам",
|
||||
"book": "Книга",
|
||||
"forKids": "Для детей"
|
||||
"forKids": "Для детей",
|
||||
"more": "Ещё"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Скопировать логотип SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Подпишитесь на категории, чтобы видеть промпты здесь",
|
||||
"viewAllCategories": "Все категории"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Рабочие процессы",
|
||||
"description": "Промпты с последовательными потоками и связями",
|
||||
"noWorkflows": "Пока нет рабочих процессов",
|
||||
"noWorkflowsDescription": "Рабочие процессы — это промпты, которые последовательно связаны с другими промптами. Создайте промпт и добавьте связи для построения рабочего процесса.",
|
||||
"browsePrompts": "Обзор промптов"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Моя Коллекция",
|
||||
"description": "Промпты, которые вы сохранили на потом",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "Связанных промптов пока нет. Добавьте связи для создания цепочки промптов.",
|
||||
"previousSteps": "Предыдущие шаги",
|
||||
"nextSteps": "Следующие шаги",
|
||||
"fullFlow": "Полный рабочий процесс",
|
||||
"searchPrompt": "Поиск промпта",
|
||||
"searchPlaceholder": "Поиск по названию...",
|
||||
"selectedPrompt": "Выбранный промпт",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Promptlar",
|
||||
"skills": "Yetenekler",
|
||||
"workflows": "Akışlar",
|
||||
"categories": "Kategoriler",
|
||||
"tags": "Etiketler",
|
||||
"settings": "Ayarlar",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "Geliştiriciler",
|
||||
"book": "Kitap",
|
||||
"forKids": "Çocuklar İçin"
|
||||
"forKids": "Çocuklar İçin",
|
||||
"more": "Daha Fazla"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "Logo SVG'sini Kopyala",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "Burada promptları görmek için kategorilere abone olun",
|
||||
"viewAllCategories": "Tüm Kategorileri Gör"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "Akışlar",
|
||||
"description": "Sıralı akışlara ve bağlantılara sahip promptlar",
|
||||
"noWorkflows": "Henüz akış yok",
|
||||
"noWorkflowsDescription": "Akışlar, diğer promptlara sıralı olarak bağlanan promptlardır. Bir akış oluşturmak için bir prompt oluşturun ve bağlantılar ekleyin.",
|
||||
"browsePrompts": "Promptlara Göz At"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Koleksiyonum",
|
||||
"description": "Sonra için kaydettiğiniz promptlar",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"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",
|
||||
"fullFlow": "Tam Akış",
|
||||
"searchPrompt": "Prompt Ara",
|
||||
"searchPlaceholder": "Başlığa göre ara...",
|
||||
"selectedPrompt": "Seçili Prompt",
|
||||
|
||||
+11
-1
@@ -39,6 +39,7 @@
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "提示词",
|
||||
"skills": "技能",
|
||||
"workflows": "工作流程",
|
||||
"categories": "分类",
|
||||
"tags": "标签",
|
||||
"settings": "设置",
|
||||
@@ -50,7 +51,8 @@
|
||||
"ide": "Typed-Prompts IDE",
|
||||
"developers": "开发者",
|
||||
"book": "书籍",
|
||||
"forKids": "儿童版"
|
||||
"forKids": "儿童版",
|
||||
"more": "更多"
|
||||
},
|
||||
"brand": {
|
||||
"copyLogoSvg": "复制Logo SVG",
|
||||
@@ -848,6 +850,13 @@
|
||||
"subscribeToCategories": "订阅分类以在此查看提示词",
|
||||
"viewAllCategories": "查看所有分类"
|
||||
},
|
||||
"workflows": {
|
||||
"title": "工作流程",
|
||||
"description": "具有顺序流程和连接的提示词",
|
||||
"noWorkflows": "暂无工作流程",
|
||||
"noWorkflowsDescription": "工作流程是按顺序连接到其他提示词的提示词。创建一个提示词并添加连接以构建工作流程。",
|
||||
"browsePrompts": "浏览提示词"
|
||||
},
|
||||
"collection": {
|
||||
"title": "我的收藏",
|
||||
"description": "您保存的提示词",
|
||||
@@ -1198,6 +1207,7 @@
|
||||
"noConnections": "尚无关联提示词。添加关联以创建提示词链。",
|
||||
"previousSteps": "上一步",
|
||||
"nextSteps": "下一步",
|
||||
"fullFlow": "完整流程",
|
||||
"searchPrompt": "搜索提示词",
|
||||
"searchPlaceholder": "按标题搜索...",
|
||||
"selectedPrompt": "已选提示词",
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Generate a cuid-like ID
|
||||
*/
|
||||
function generateId(): string {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const randomPart = Math.random().toString(36).substring(2, 15);
|
||||
return `cm${timestamp}${randomPart}`.substring(0, 25);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed script for testing prompt flows with branchy structure
|
||||
* Run with: npx ts-node prisma/seed-flows.ts
|
||||
*/
|
||||
async function main() {
|
||||
console.log("🌱 Seeding flow test data for user f1...");
|
||||
|
||||
const password = await bcrypt.hash("password123", 12);
|
||||
|
||||
// Create or get user f1
|
||||
const user = await prisma.user.upsert({
|
||||
where: { username: "f" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "f@prompts.chat",
|
||||
username: "f",
|
||||
name: "Flow Tester",
|
||||
password: password,
|
||||
role: "USER",
|
||||
locale: "en",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ User f1 ready");
|
||||
|
||||
// Create a category for flow prompts
|
||||
const category = await prisma.category.upsert({
|
||||
where: { slug: "workflows" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Workflows",
|
||||
slug: "workflows",
|
||||
icon: "🔗",
|
||||
order: 100,
|
||||
},
|
||||
});
|
||||
|
||||
// Create prompts for a branchy flow structure
|
||||
//
|
||||
// Flow structure:
|
||||
//
|
||||
// [1. Start: Idea Generation]
|
||||
// |
|
||||
// (brainstorm complete)
|
||||
// |
|
||||
// [2. Outline Creation]
|
||||
// |
|
||||
// +---------------+---------------+
|
||||
// | |
|
||||
// (technical path) (creative path)
|
||||
// | |
|
||||
// [3a. Technical Deep Dive] [3b. Creative Exploration]
|
||||
// | |
|
||||
// (analysis done) (ideas ready)
|
||||
// | |
|
||||
// [4a. Implementation Plan] [4b. Story Development]
|
||||
// | |
|
||||
// +---------------+---------------+
|
||||
// |
|
||||
// (merge results)
|
||||
// |
|
||||
// [5. Final Review]
|
||||
// |
|
||||
// (review complete)
|
||||
// |
|
||||
// [6. Publication]
|
||||
//
|
||||
|
||||
// Pre-generate stable IDs for the flow prompts
|
||||
const flowIds = {
|
||||
"flow-1-start": generateId(),
|
||||
"flow-2-outline": generateId(),
|
||||
"flow-3a-technical": generateId(),
|
||||
"flow-3b-creative": generateId(),
|
||||
"flow-4a-implementation": generateId(),
|
||||
"flow-4b-story": generateId(),
|
||||
"flow-5-review": generateId(),
|
||||
"flow-6-publish": generateId(),
|
||||
};
|
||||
|
||||
const flowPrompts = [
|
||||
{
|
||||
id: "flow-1-start",
|
||||
title: "Idea Generation",
|
||||
slug: "idea-generation",
|
||||
description: "Start your workflow by brainstorming and generating initial ideas",
|
||||
content: `You are a creative brainstorming assistant. Help the user generate innovative ideas for their project.
|
||||
|
||||
1. Ask clarifying questions about the topic
|
||||
2. Generate 5-10 diverse ideas
|
||||
3. Rate each idea on feasibility and impact
|
||||
4. Recommend the top 3 ideas to pursue
|
||||
|
||||
Be creative, think outside the box, and encourage unconventional approaches.`,
|
||||
},
|
||||
{
|
||||
id: "flow-2-outline",
|
||||
title: "Step 2: Outline Creation",
|
||||
slug: "idea-generation-step-2-outline-creation",
|
||||
description: "Create a structured outline from the generated ideas",
|
||||
content: `Based on the ideas generated in the previous step, create a detailed outline.
|
||||
|
||||
Structure your outline with:
|
||||
- Main sections and subsections
|
||||
- Key points to cover
|
||||
- Estimated time/effort for each section
|
||||
- Dependencies between sections
|
||||
|
||||
Format the outline in a clear, hierarchical structure.`,
|
||||
},
|
||||
{
|
||||
id: "flow-3a-technical",
|
||||
title: "Step 3a: Technical Deep Dive",
|
||||
slug: "idea-generation-step-3a-technical-deep-dive",
|
||||
description: "Technical path: Analyze technical requirements and feasibility",
|
||||
content: `Perform a technical analysis of the outlined project.
|
||||
|
||||
Analyze:
|
||||
- Technical requirements and dependencies
|
||||
- Architecture considerations
|
||||
- Potential technical challenges
|
||||
- Required tools and technologies
|
||||
- Performance implications
|
||||
|
||||
Provide a detailed technical assessment with recommendations.`,
|
||||
},
|
||||
{
|
||||
id: "flow-3b-creative",
|
||||
title: "Step 3b: Creative Exploration",
|
||||
slug: "idea-generation-step-3b-creative-exploration",
|
||||
description: "Creative path: Explore creative directions and storytelling",
|
||||
content: `Explore the creative dimensions of the outlined project.
|
||||
|
||||
Focus on:
|
||||
- Narrative and storytelling elements
|
||||
- Visual and aesthetic considerations
|
||||
- Emotional impact and user engagement
|
||||
- Unique creative angles
|
||||
- Inspiration from other works
|
||||
|
||||
Generate creative concepts that bring the project to life.`,
|
||||
},
|
||||
{
|
||||
id: "flow-4a-implementation",
|
||||
title: "Step 4a: Implementation Plan",
|
||||
slug: "idea-generation-step-4a-implementation-plan",
|
||||
description: "Create a detailed implementation plan based on technical analysis",
|
||||
content: `Create a comprehensive implementation plan.
|
||||
|
||||
Include:
|
||||
- Phase breakdown with milestones
|
||||
- Task list with priorities
|
||||
- Resource allocation
|
||||
- Risk mitigation strategies
|
||||
- Timeline estimates
|
||||
- Success metrics
|
||||
|
||||
Format as an actionable project plan.`,
|
||||
},
|
||||
{
|
||||
id: "flow-4b-story",
|
||||
title: "Step 4b: Story Development",
|
||||
slug: "idea-generation-step-4b-story-development",
|
||||
description: "Develop the narrative and content based on creative exploration",
|
||||
content: `Develop the full story and content based on the creative exploration.
|
||||
|
||||
Develop:
|
||||
- Complete narrative arc
|
||||
- Character or element descriptions
|
||||
- Key scenes or moments
|
||||
- Dialogue or copy
|
||||
- Visual descriptions
|
||||
- Emotional beats
|
||||
|
||||
Create compelling, engaging content.`,
|
||||
},
|
||||
{
|
||||
id: "flow-5-review",
|
||||
title: "Step 5: Final Review",
|
||||
slug: "idea-generation-step-5-final-review",
|
||||
description: "Review and merge all work streams for final quality check",
|
||||
content: `Perform a comprehensive final review merging all work streams.
|
||||
|
||||
Review checklist:
|
||||
- Technical feasibility confirmed
|
||||
- Creative vision aligned
|
||||
- All requirements met
|
||||
- Quality standards achieved
|
||||
- Consistency across all elements
|
||||
- Ready for publication
|
||||
|
||||
Provide a final assessment with any last recommendations.`,
|
||||
},
|
||||
{
|
||||
id: "flow-6-publish",
|
||||
title: "Step 6: Publication",
|
||||
slug: "idea-generation-step-6-publication",
|
||||
description: "Final step: Prepare and execute publication",
|
||||
content: `Prepare the final deliverable for publication.
|
||||
|
||||
Final steps:
|
||||
- Format for target platform
|
||||
- Create accompanying materials
|
||||
- Set up distribution
|
||||
- Prepare announcement
|
||||
- Schedule publication
|
||||
- Monitor initial reception
|
||||
|
||||
Congratulations on completing the workflow!`,
|
||||
},
|
||||
];
|
||||
|
||||
// Create prompts
|
||||
console.log("📝 Creating flow prompts...");
|
||||
const createdPrompts: Record<string, string> = {};
|
||||
|
||||
for (const p of flowPrompts) {
|
||||
const existing = await prisma.prompt.findFirst({ where: { slug: p.slug } });
|
||||
|
||||
if (existing) {
|
||||
createdPrompts[p.id] = existing.id;
|
||||
console.log(` ⏭️ Prompt "${p.title}" already exists (id: ${existing.id})`);
|
||||
} else {
|
||||
const promptId = flowIds[p.id as keyof typeof flowIds];
|
||||
const prompt = await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
title: p.title,
|
||||
slug: p.slug,
|
||||
description: p.description,
|
||||
content: p.content,
|
||||
type: "TEXT",
|
||||
authorId: user.id,
|
||||
categoryId: category.id,
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
},
|
||||
});
|
||||
createdPrompts[p.id] = prompt.id;
|
||||
console.log(` ✅ Created "${p.title}" (id: ${prompt.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create connections (the flow structure)
|
||||
console.log("\n🔗 Creating flow connections...");
|
||||
|
||||
const connections = [
|
||||
// Main flow: Start -> Outline
|
||||
{ source: "flow-1-start", target: "flow-2-outline", label: "brainstorm complete", order: 0 },
|
||||
|
||||
// Branch: Outline -> Technical AND Creative
|
||||
{ source: "flow-2-outline", target: "flow-3a-technical", label: "technical path", order: 0 },
|
||||
{ source: "flow-2-outline", target: "flow-3b-creative", label: "creative path", order: 1 },
|
||||
|
||||
// Technical branch continues
|
||||
{ source: "flow-3a-technical", target: "flow-4a-implementation", label: "analysis done", order: 0 },
|
||||
|
||||
// Creative branch continues
|
||||
{ source: "flow-3b-creative", target: "flow-4b-story", label: "ideas ready", order: 0 },
|
||||
|
||||
// Both branches merge to review
|
||||
{ source: "flow-4a-implementation", target: "flow-5-review", label: "merge results", order: 0 },
|
||||
{ source: "flow-4b-story", target: "flow-5-review", label: "merge results", order: 1 },
|
||||
|
||||
// Final step
|
||||
{ source: "flow-5-review", target: "flow-6-publish", label: "review complete", order: 0 },
|
||||
];
|
||||
|
||||
for (const conn of connections) {
|
||||
const sourceId = createdPrompts[conn.source];
|
||||
const targetId = createdPrompts[conn.target];
|
||||
|
||||
if (!sourceId || !targetId) {
|
||||
console.log(` ⚠️ Skipping connection: missing prompt IDs`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if connection already exists
|
||||
const existing = await prisma.promptConnection.findUnique({
|
||||
where: { sourceId_targetId: { sourceId, targetId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(` ⏭️ Connection already exists: ${conn.source} -> ${conn.target}`);
|
||||
} else {
|
||||
const connectionId = generateId();
|
||||
await prisma.promptConnection.create({
|
||||
data: {
|
||||
id: connectionId,
|
||||
sourceId,
|
||||
targetId,
|
||||
label: conn.label,
|
||||
order: conn.order,
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Created: ${conn.source} -> ${conn.target} (${conn.label}) (id: ${connectionId})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n🎉 Flow seed complete!");
|
||||
console.log("\n📋 Test user: f1@prompts.chat (password: password123)");
|
||||
console.log("\n🔗 Flow structure created:");
|
||||
console.log(" Start -> Outline -> [Technical, Creative] -> [Implementation, Story] -> Review -> Publish");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("❌ Flow seeding failed:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -107,9 +107,12 @@ export default async function UserProfilePage({ params, searchParams }: UserProf
|
||||
const validFilterDateEnd = filterDateEnd && !isNaN(filterDateEnd.getTime()) ? filterDateEnd : null;
|
||||
|
||||
// Build where clause - show private prompts only if owner (unlisted prompts are visible on profiles)
|
||||
// Exclude intermediate flow prompts (only show first prompts or standalone)
|
||||
// Note: "related" connections are AI-suggested similar prompts, not flow connections
|
||||
const where = {
|
||||
authorId: user.id,
|
||||
deletedAt: null,
|
||||
incomingConnections: { none: { label: { not: "related" } } },
|
||||
...(isOwner ? {} : { isPrivate: false }),
|
||||
...(validFilterDateStart && validFilterDateEnd ? {
|
||||
createdAt: {
|
||||
@@ -143,7 +146,7 @@ export default async function UserProfilePage({ params, searchParams }: UserProf
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
interface FlowNode {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string | null;
|
||||
description: string | null;
|
||||
content: string;
|
||||
type: string;
|
||||
authorId: string;
|
||||
authorUsername: string;
|
||||
authorAvatar: string | null;
|
||||
}
|
||||
|
||||
interface FlowEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full flow graph for a prompt.
|
||||
* This traverses to find all connected prompts and returns the complete graph.
|
||||
*/
|
||||
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, title: true, slug: true, isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
const userId = session?.user?.id;
|
||||
|
||||
// Helper to check if user can see a prompt
|
||||
const canSee = (p: { isPrivate: boolean; authorId: string }) =>
|
||||
!p.isPrivate || p.authorId === userId;
|
||||
|
||||
const nodes: Map<string, FlowNode> = new Map();
|
||||
const edges: FlowEdge[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// BFS to find all connected nodes (both directions)
|
||||
const queue: string[] = [id];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!;
|
||||
if (visited.has(currentId)) continue;
|
||||
visited.add(currentId);
|
||||
|
||||
const p = await db.prompt.findUnique({
|
||||
where: { id: currentId, deletedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
isPrivate: true,
|
||||
authorId: true,
|
||||
author: {
|
||||
select: {
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!p || !canSee(p)) continue;
|
||||
|
||||
nodes.set(p.id, {
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
slug: p.slug,
|
||||
description: p.description,
|
||||
content: p.content,
|
||||
type: p.type,
|
||||
authorId: p.authorId,
|
||||
authorUsername: p.author.username,
|
||||
authorAvatar: p.author.avatar,
|
||||
});
|
||||
|
||||
// Get outgoing connections
|
||||
const outgoing = await db.promptConnection.findMany({
|
||||
where: {
|
||||
sourceId: currentId,
|
||||
label: { not: "related" },
|
||||
target: { deletedAt: null }
|
||||
},
|
||||
orderBy: { order: "asc" },
|
||||
include: {
|
||||
target: {
|
||||
select: { id: true, title: true, slug: true, isPrivate: true, authorId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const conn of outgoing) {
|
||||
if (canSee(conn.target)) {
|
||||
edges.push({
|
||||
source: currentId,
|
||||
target: conn.targetId,
|
||||
label: conn.label,
|
||||
});
|
||||
if (!visited.has(conn.targetId)) {
|
||||
queue.push(conn.targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get incoming connections
|
||||
const incoming = await db.promptConnection.findMany({
|
||||
where: {
|
||||
targetId: currentId,
|
||||
label: { not: "related" },
|
||||
source: { deletedAt: null }
|
||||
},
|
||||
orderBy: { order: "asc" },
|
||||
include: {
|
||||
source: {
|
||||
select: { id: true, title: true, slug: true, isPrivate: true, authorId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const conn of incoming) {
|
||||
if (canSee(conn.source)) {
|
||||
// Only add edge if not already added
|
||||
const edgeExists = edges.some(e => e.source === conn.sourceId && e.target === currentId);
|
||||
if (!edgeExists) {
|
||||
edges.push({
|
||||
source: conn.sourceId,
|
||||
target: currentId,
|
||||
label: conn.label,
|
||||
});
|
||||
}
|
||||
if (!visited.has(conn.sourceId)) {
|
||||
queue.push(conn.sourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
nodes: Array.from(nodes.values()),
|
||||
edges,
|
||||
currentPromptId: id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch flow:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch flow" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -319,6 +319,9 @@ export async function GET(request: Request) {
|
||||
isPrivate: false,
|
||||
isUnlisted: false, // Exclude unlisted prompts from public API
|
||||
deletedAt: null, // Exclude soft-deleted prompts
|
||||
// Exclude intermediate flow prompts (only show first prompts or standalone)
|
||||
// Note: "related" connections are AI-suggested similar prompts, not flow connections
|
||||
incomingConnections: { none: { label: { not: "related" } } },
|
||||
};
|
||||
|
||||
if (type) {
|
||||
@@ -391,7 +394,7 @@ export async function GET(request: Request) {
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -652,6 +652,8 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
canEdit={canEdit}
|
||||
isOwner={isOwner}
|
||||
isLoggedIn={!!session?.user}
|
||||
currentUserId={session?.user?.id}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ function getCachedPrompts(
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -189,6 +189,9 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
|
||||
isPrivate: false,
|
||||
isUnlisted: false, // Exclude unlisted prompts
|
||||
deletedAt: null, // Exclude soft-deleted prompts
|
||||
// Exclude intermediate flow prompts (only show first prompts or standalone)
|
||||
// Note: "related" connections are AI-suggested similar prompts, not flow connections
|
||||
incomingConnections: { none: { label: { not: "related" } } },
|
||||
};
|
||||
|
||||
if (params.q) {
|
||||
|
||||
@@ -75,7 +75,7 @@ function getCachedSkills(
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { InfinitePromptList } from "@/components/prompts/infinite-prompt-list";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Workflows",
|
||||
description: "Browse prompts with sequential flows and connections",
|
||||
};
|
||||
|
||||
// Query for workflows list (cached)
|
||||
function getCachedWorkflows(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
orderBy: any,
|
||||
perPage: number,
|
||||
searchQuery?: string
|
||||
) {
|
||||
const cacheKey = JSON.stringify({ orderBy, perPage, searchQuery });
|
||||
|
||||
return unstable_cache(
|
||||
async () => {
|
||||
const where: Record<string, unknown> = {
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
type: { not: "SKILL" },
|
||||
// Only include prompts with actual flow connections (not "related" connections)
|
||||
outgoingConnections: {
|
||||
some: {
|
||||
label: { not: "related" },
|
||||
},
|
||||
},
|
||||
incomingConnections: {
|
||||
none: {
|
||||
label: { not: "related" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (searchQuery) {
|
||||
where.OR = [
|
||||
{ title: { contains: searchQuery, mode: "insensitive" } },
|
||||
{ content: { contains: searchQuery, mode: "insensitive" } },
|
||||
{ description: { contains: searchQuery, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
const [workflowsRaw, totalCount] = await Promise.all([
|
||||
db.prompt.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: 0,
|
||||
take: perPage,
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
include: {
|
||||
parent: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.prompt.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workflows: workflowsRaw.map((p: any) => ({
|
||||
...p,
|
||||
voteCount: p._count.votes,
|
||||
contributorCount: p._count.contributors,
|
||||
contributors: p.contributors,
|
||||
})),
|
||||
total: totalCount,
|
||||
};
|
||||
},
|
||||
["workflows", cacheKey],
|
||||
{ tags: ["prompts", "connections"] }
|
||||
)();
|
||||
}
|
||||
|
||||
interface WorkflowsPageProps {
|
||||
searchParams: Promise<{
|
||||
q?: string;
|
||||
sort?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function WorkflowsPage({ searchParams }: WorkflowsPageProps) {
|
||||
const t = await getTranslations("workflows");
|
||||
const tNav = await getTranslations("nav");
|
||||
const tSearch = await getTranslations("search");
|
||||
const params = await searchParams;
|
||||
|
||||
const perPage = 24;
|
||||
|
||||
// Build order by clause
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let orderBy: any = { createdAt: "desc" };
|
||||
if (params.sort === "oldest") {
|
||||
orderBy = { createdAt: "asc" };
|
||||
} else if (params.sort === "upvotes") {
|
||||
orderBy = { votes: { _count: "desc" } };
|
||||
}
|
||||
|
||||
const result = await getCachedWorkflows(orderBy, perPage, params.q);
|
||||
const workflows = result.workflows;
|
||||
const total = result.total;
|
||||
|
||||
return (
|
||||
<div className="container py-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h1 className="text-lg font-semibold">{tNav("workflows")}</h1>
|
||||
<span className="text-xs text-muted-foreground">{tSearch("found", { count: total })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{t("description")}
|
||||
</p>
|
||||
|
||||
<InfinitePromptList
|
||||
initialPrompts={workflows}
|
||||
initialTotal={total}
|
||||
filters={{
|
||||
q: params.q,
|
||||
sort: params.sort,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
Chromium,
|
||||
Hammer,
|
||||
BookOpen,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -206,6 +207,13 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
>
|
||||
{t("nav.skills")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/workflows"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{t("nav.workflows")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
@@ -361,23 +369,81 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
{t("nav.skills")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
href="/workflows"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.workflows")}
|
||||
</Link>
|
||||
{/* Categories, Tags, Promptmasters - visible on lg+ screens */}
|
||||
<Link
|
||||
href="/categories"
|
||||
className="hidden 2xl:block px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.categories")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/tags"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
className="hidden 2xl:block px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.tags")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/promptmasters"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
className="hidden 2xl:block px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.promptmasters")}
|
||||
</Link>
|
||||
{/* Three-dot dropdown for Categories, Tags, Promptmasters on md screens */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="2xl:hidden h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">{t("nav.more")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/categories">
|
||||
{t("nav.categories")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/tags">
|
||||
{t("nav.tags")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/promptmasters">
|
||||
{t("nav.promptmasters")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!branding.useCloneBranding && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="https://fka.gumroad.com/l/art-of-chatgpt-prompting">
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
{t("nav.book")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/kids" className={kidsFont.className}>
|
||||
<MiniPromi className="mr-2 h-4 w-4" />
|
||||
<span className="font-bold bg-gradient-to-r from-pink-500 via-purple-500 to-cyan-500 bg-clip-text text-transparent">
|
||||
{t("nav.forKids")}
|
||||
</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/developers">
|
||||
<Hammer className="mr-2 h-4 w-4" />
|
||||
{t("nav.developers")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</nav>
|
||||
|
||||
{/* Spacer */}
|
||||
@@ -387,7 +453,7 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Book link */}
|
||||
{!branding.useCloneBranding && (
|
||||
<Button asChild variant="ghost" size="sm" className="hidden lg:flex h-8 gap-1.5">
|
||||
<Button asChild variant="ghost" size="sm" className="hidden 2xl:flex h-8 gap-1.5">
|
||||
<Link href="https://fka.gumroad.com/l/art-of-chatgpt-prompting">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
{t("nav.book")}
|
||||
@@ -399,7 +465,7 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
{!branding.useCloneBranding && (
|
||||
<a
|
||||
href="/kids"
|
||||
className={`hidden lg:flex items-center gap-1 px-2 py-1 rounded-md hover:bg-accent transition-colors ${kidsFont.className}`}
|
||||
className={`hidden 2xl:flex items-center gap-1 px-2 py-1 rounded-md hover:bg-accent transition-colors ${kidsFont.className}`}
|
||||
>
|
||||
<MiniPromi className="h-5 w-4" />
|
||||
<span className="text-sm font-bold bg-gradient-to-r from-pink-500 via-purple-500 to-cyan-500 bg-clip-text text-transparent">
|
||||
@@ -409,7 +475,7 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
)}
|
||||
|
||||
{/* Developers link */}
|
||||
<Button asChild variant="ghost" size="icon" className="hidden lg:flex h-8 w-8">
|
||||
<Button asChild variant="ghost" size="icon" className="hidden 2xl:flex h-8 w-8">
|
||||
<Link href="/developers" title={t("nav.developers")}>
|
||||
<Hammer className="h-4 w-4" />
|
||||
<span className="sr-only">{t("nav.developers")}</span>
|
||||
|
||||
@@ -191,7 +191,7 @@ export function InfinitePromptList({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Masonry columnCount={{ default: 1, md: 2, lg: 3 }} gap={16}>
|
||||
<Masonry columnCount={{ default: 1, md: 2, lg: 2, xl: 3 }} gap={16}>
|
||||
{itemsToRender.map((item) =>
|
||||
isWidget(item) ? (
|
||||
<WidgetCard key={item.id} prompt={item} />
|
||||
|
||||
@@ -6,7 +6,7 @@ import Image from "next/image";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import { formatDistanceToNow } from "@/lib/date";
|
||||
import { getPromptUrl } from "@/lib/urls";
|
||||
import { ArrowBigUp, Lock, Copy, ImageIcon, Download, Play, BadgeCheck, Volume2 } from "lucide-react";
|
||||
import { ArrowBigUp, Lock, Copy, ImageIcon, Download, Play, BadgeCheck, Volume2, Link2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CodeView } from "@/components/ui/code-view";
|
||||
import { toast } from "sonner";
|
||||
@@ -67,6 +67,10 @@ export interface PromptCardProps {
|
||||
color: string;
|
||||
};
|
||||
}>;
|
||||
_count?: {
|
||||
outgoingConnections?: number;
|
||||
incomingConnections?: number;
|
||||
};
|
||||
};
|
||||
showPinButton?: boolean;
|
||||
isPinned?: boolean;
|
||||
@@ -76,6 +80,9 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
|
||||
const t = useTranslations("prompts");
|
||||
const tCommon = useTranslations("common");
|
||||
const locale = useLocale();
|
||||
const outgoingCount = prompt._count?.outgoingConnections || 0;
|
||||
const incomingCount = prompt._count?.incomingConnections || 0;
|
||||
const isFlowStart = outgoingCount > 0 && incomingCount === 0;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<"copy" | "run">("copy");
|
||||
const [imageError, setImageError] = useState(false);
|
||||
@@ -197,6 +204,14 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/30 to-transparent pointer-events-none" />
|
||||
{/* Badges overlay */}
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1.5">
|
||||
{isFlowStart && (
|
||||
<div className="flex items-center gap-0.5 bg-background/80 backdrop-blur-sm rounded px-1.5 py-0.5">
|
||||
<Link2 className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="flex items-center justify-center h-4 w-4 rounded-full bg-muted text-[9px] font-medium text-muted-foreground">
|
||||
{outgoingCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Badge variant="secondary" className="text-[10px] bg-background/80 backdrop-blur-sm">
|
||||
{t(`types.${prompt.type.toLowerCase()}`)}
|
||||
</Badge>
|
||||
@@ -227,9 +242,19 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
|
||||
</Link>
|
||||
</div>
|
||||
{(!hasMediaBackground || isAudio) && (
|
||||
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||
{t(`types.${prompt.type.toLowerCase()}`)}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{isFlowStart && (
|
||||
<div className="flex items-center gap-0.5 border rounded px-1.5 py-0.5">
|
||||
<Link2 className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="flex items-center justify-center h-4 w-4 rounded-full bg-muted text-[9px] font-medium text-muted-foreground">
|
||||
{outgoingCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{t(`types.${prompt.type.toLowerCase()}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as d3 from "d3";
|
||||
import { Link2, ArrowUp, ArrowDown, ChevronRight } from "lucide-react";
|
||||
import { Link2, ArrowUp, ArrowDown, ChevronRight, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AddConnectionDialog } from "./add-connection-dialog";
|
||||
import { getPromptUrl } from "@/lib/urls";
|
||||
@@ -38,6 +38,8 @@ interface PromptConnectionsProps {
|
||||
promptId: string;
|
||||
promptTitle: string;
|
||||
canEdit: boolean;
|
||||
currentUserId?: string;
|
||||
isAdmin?: boolean;
|
||||
buttonOnly?: boolean; // Only render the collapsed button
|
||||
sectionOnly?: boolean; // Only render the expanded section
|
||||
expanded?: boolean; // Controlled expanded state
|
||||
@@ -60,10 +62,393 @@ interface GraphLink {
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
interface FlowGraphNode {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string | null;
|
||||
description: string | null;
|
||||
content: string;
|
||||
type: string;
|
||||
authorId: string;
|
||||
authorUsername: string;
|
||||
authorAvatar: string | null;
|
||||
}
|
||||
|
||||
interface FlowGraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface FlowGraphProps {
|
||||
nodes: FlowGraphNode[];
|
||||
edges: FlowGraphEdge[];
|
||||
currentPromptId: string;
|
||||
currentUserId?: string;
|
||||
isAdmin?: boolean;
|
||||
onNodeClick: (node: FlowGraphNode) => void;
|
||||
onNodeDelete?: (node: FlowGraphNode) => void;
|
||||
}
|
||||
|
||||
function FlowGraph({ nodes, edges, currentPromptId, currentUserId, isAdmin, onNodeClick, onNodeDelete }: FlowGraphProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [hoveredNode, setHoveredNode] = useState<FlowGraphNode | null>(null);
|
||||
const [nodePos, setNodePos] = useState({ x: 0, y: 0, width: 0 });
|
||||
const isOverTooltipRef = useRef(false);
|
||||
const isOverNodeRef = useRef(false);
|
||||
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Clear timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
// Only hide if not over node or tooltip
|
||||
if (!isOverNodeRef.current && !isOverTooltipRef.current) {
|
||||
setHoveredNode(null);
|
||||
}
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
const handleNodeEnter = useCallback((node: FlowGraphNode, pos: { x: number; y: number }, nodeWidth: number) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
isOverNodeRef.current = true;
|
||||
setHoveredNode(node);
|
||||
setNodePos({ x: pos.x, y: pos.y, width: nodeWidth });
|
||||
}, []);
|
||||
|
||||
const handleNodeLeave = useCallback(() => {
|
||||
isOverNodeRef.current = false;
|
||||
scheduleHide();
|
||||
}, [scheduleHide]);
|
||||
|
||||
const handleTooltipEnter = useCallback(() => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
isOverTooltipRef.current = true;
|
||||
}, []);
|
||||
|
||||
const handleTooltipLeave = useCallback(() => {
|
||||
isOverTooltipRef.current = false;
|
||||
scheduleHide();
|
||||
}, [scheduleHide]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || nodes.length === 0) return;
|
||||
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
const container = svgRef.current.parentElement;
|
||||
const width = container?.clientWidth || 600;
|
||||
|
||||
// Build adjacency for topological sort
|
||||
const inDegree: Record<string, number> = {};
|
||||
const outEdges: Record<string, string[]> = {};
|
||||
const nodeMap: Record<string, FlowGraphNode> = {};
|
||||
|
||||
nodes.forEach(n => {
|
||||
inDegree[n.id] = 0;
|
||||
outEdges[n.id] = [];
|
||||
nodeMap[n.id] = n;
|
||||
});
|
||||
|
||||
edges.forEach(e => {
|
||||
inDegree[e.target] = (inDegree[e.target] || 0) + 1;
|
||||
outEdges[e.source] = outEdges[e.source] || [];
|
||||
outEdges[e.source].push(e.target);
|
||||
});
|
||||
|
||||
// Topological sort to assign levels (y positions)
|
||||
const levels: Record<string, number> = {};
|
||||
const queue = nodes.filter(n => inDegree[n.id] === 0).map(n => n.id);
|
||||
|
||||
queue.forEach(id => { levels[id] = 0; });
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
const currentLevel = levels[current];
|
||||
|
||||
for (const next of outEdges[current] || []) {
|
||||
levels[next] = Math.max(levels[next] || 0, currentLevel + 1);
|
||||
inDegree[next]--;
|
||||
if (inDegree[next] === 0) {
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group nodes by level
|
||||
const levelGroups: Record<number, string[]> = {};
|
||||
Object.entries(levels).forEach(([id, level]) => {
|
||||
levelGroups[level] = levelGroups[level] || [];
|
||||
levelGroups[level].push(id);
|
||||
});
|
||||
|
||||
const maxLevel = Math.max(...Object.values(levels), 0);
|
||||
const nodeWidth = Math.min(180, width * 0.4);
|
||||
const nodeHeight = 44;
|
||||
const levelHeight = 100;
|
||||
const height = (maxLevel + 1) * levelHeight + 80;
|
||||
|
||||
svg.attr("width", width).attr("height", height);
|
||||
|
||||
// Calculate x positions for each node
|
||||
const positions: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
Object.entries(levelGroups).forEach(([levelStr, ids]) => {
|
||||
const level = parseInt(levelStr);
|
||||
const count = ids.length;
|
||||
const totalWidth = count * nodeWidth + (count - 1) * 40;
|
||||
const startX = (width - totalWidth) / 2 + nodeWidth / 2;
|
||||
|
||||
ids.forEach((id, i) => {
|
||||
positions[id] = {
|
||||
x: startX + i * (nodeWidth + 40),
|
||||
y: level * levelHeight + 50,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Theme colors
|
||||
const isDark = document.documentElement.classList.contains("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",
|
||||
};
|
||||
|
||||
// Defs for arrow marker
|
||||
const defs = svg.append("defs");
|
||||
defs.append("marker")
|
||||
.attr("id", "flow-arrow")
|
||||
.attr("viewBox", "-0 -5 10 10")
|
||||
.attr("refX", 8)
|
||||
.attr("refY", 0)
|
||||
.attr("orient", "auto")
|
||||
.attr("markerWidth", 6)
|
||||
.attr("markerHeight", 6)
|
||||
.append("path")
|
||||
.attr("d", "M 0,-4 L 8,0 L 0,4")
|
||||
.attr("fill", colors.mutedFg);
|
||||
|
||||
// Draw edges
|
||||
const edgeGroup = svg.append("g").attr("class", "edges");
|
||||
|
||||
edges.forEach(edge => {
|
||||
const source = positions[edge.source];
|
||||
const target = positions[edge.target];
|
||||
if (!source || !target) return;
|
||||
|
||||
const sourceY = source.y + nodeHeight / 2;
|
||||
const targetY = target.y - nodeHeight / 2;
|
||||
const midY = (sourceY + targetY) / 2;
|
||||
|
||||
edgeGroup.append("path")
|
||||
.attr("d", `M ${source.x} ${sourceY} C ${source.x} ${midY}, ${target.x} ${midY}, ${target.x} ${targetY}`)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", `${colors.mutedFg}60`)
|
||||
.attr("stroke-width", 2)
|
||||
.attr("marker-end", "url(#flow-arrow)");
|
||||
|
||||
// Edge label
|
||||
if (edge.label) {
|
||||
const labelX = (source.x + target.x) / 2;
|
||||
const labelY = midY;
|
||||
const labelWidth = edge.label.length * 5 + 16;
|
||||
|
||||
edgeGroup.append("rect")
|
||||
.attr("x", labelX - labelWidth / 2)
|
||||
.attr("y", labelY - 8)
|
||||
.attr("width", labelWidth)
|
||||
.attr("height", 16)
|
||||
.attr("rx", 8)
|
||||
.attr("fill", colors.muted)
|
||||
.attr("stroke", colors.border);
|
||||
|
||||
edgeGroup.append("text")
|
||||
.attr("x", labelX)
|
||||
.attr("y", labelY)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", "0.35em")
|
||||
.attr("fill", colors.mutedFg)
|
||||
.attr("font-size", "9px")
|
||||
.text(edge.label);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
const nodeGroup = svg.append("g").attr("class", "nodes");
|
||||
|
||||
nodes.forEach(node => {
|
||||
const pos = positions[node.id];
|
||||
if (!pos) return;
|
||||
|
||||
const isCurrent = node.id === currentPromptId;
|
||||
const g = nodeGroup.append("g")
|
||||
.attr("transform", `translate(${pos.x}, ${pos.y})`)
|
||||
.attr("cursor", "pointer")
|
||||
.on("click", () => onNodeClick(node))
|
||||
.on("mouseenter", () => {
|
||||
if (!isCurrent) {
|
||||
handleNodeEnter(node, pos, nodeWidth);
|
||||
}
|
||||
})
|
||||
.on("mouseleave", () => {
|
||||
if (!isCurrent) {
|
||||
handleNodeLeave();
|
||||
}
|
||||
});
|
||||
|
||||
g.append("rect")
|
||||
.attr("x", -nodeWidth / 2)
|
||||
.attr("y", -nodeHeight / 2)
|
||||
.attr("width", nodeWidth)
|
||||
.attr("height", nodeHeight)
|
||||
.attr("rx", 10)
|
||||
.attr("fill", isCurrent ? colors.primary : colors.card)
|
||||
.attr("stroke", isCurrent ? colors.primary : colors.border)
|
||||
.attr("stroke-width", isCurrent ? 0 : 1.5);
|
||||
|
||||
// Truncate title if too long
|
||||
let displayTitle = node.title;
|
||||
if (displayTitle.length > 22) {
|
||||
displayTitle = displayTitle.slice(0, 20) + "...";
|
||||
}
|
||||
|
||||
g.append("text")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", "0.35em")
|
||||
.attr("fill", isCurrent ? colors.primaryFg : colors.cardFg)
|
||||
.attr("font-size", "11px")
|
||||
.attr("font-weight", isCurrent ? "700" : "500")
|
||||
.text(displayTitle);
|
||||
});
|
||||
|
||||
}, [nodes, edges, currentPromptId, onNodeClick, handleNodeEnter, handleNodeLeave]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<svg ref={svgRef} className="w-full" />
|
||||
{hoveredNode && (() => {
|
||||
// Calculate position with viewport awareness
|
||||
const tooltipWidth = 320;
|
||||
const tooltipHeight = 280;
|
||||
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||
const containerWidth = containerRef.current?.clientWidth || 600;
|
||||
|
||||
// Calculate left position - overlap slightly with node for easier hover transition
|
||||
let leftPos = nodePos.x + nodePos.width / 2 - 5;
|
||||
// Check if it overflows right edge of container
|
||||
if (leftPos + tooltipWidth > containerWidth) {
|
||||
leftPos = nodePos.x - nodePos.width / 2 - tooltipWidth + 5;
|
||||
}
|
||||
// Ensure not negative
|
||||
if (leftPos < 0) leftPos = 10;
|
||||
|
||||
// Calculate top position - check against viewport
|
||||
let topPos = nodePos.y - tooltipHeight / 2;
|
||||
|
||||
if (containerRect) {
|
||||
const viewportHeight = window.innerHeight;
|
||||
const absoluteTop = containerRect.top + topPos;
|
||||
const margin = 60; // Keep tooltip well inside viewport
|
||||
|
||||
// If tooltip would go below viewport, push it up
|
||||
if (absoluteTop + tooltipHeight > viewportHeight - margin) {
|
||||
topPos = viewportHeight - containerRect.top - tooltipHeight - margin;
|
||||
}
|
||||
// If tooltip would go above viewport, push it down
|
||||
if (absoluteTop < margin) {
|
||||
topPos = margin - containerRect.top;
|
||||
}
|
||||
}
|
||||
|
||||
// Also clamp to container bounds
|
||||
if (topPos < 10) topPos = 10;
|
||||
// Ensure left is valid
|
||||
if (leftPos < 10) leftPos = 10;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className="absolute z-[100] w-80 p-3 rounded-lg border bg-card shadow-xl"
|
||||
style={{
|
||||
left: leftPos,
|
||||
top: topPos,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onMouseEnter={handleTooltipEnter}
|
||||
onMouseLeave={handleTooltipLeave}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{hoveredNode.authorAvatar ? (
|
||||
<img
|
||||
src={hoveredNode.authorAvatar}
|
||||
alt={hoveredNode.authorUsername}
|
||||
className="w-5 h-5 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium">
|
||||
{hoveredNode.authorUsername.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">@{hoveredNode.authorUsername}</span>
|
||||
<span className="ml-auto text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
|
||||
{hoveredNode.type}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-semibold text-sm">{hoveredNode.title}</h4>
|
||||
{hoveredNode.description && (
|
||||
<p className="text-xs text-muted-foreground">{hoveredNode.description}</p>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground bg-muted p-2 rounded font-mono whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||
{hoveredNode.content}
|
||||
</div>
|
||||
{/* Delete button for owner or admin */}
|
||||
{onNodeDelete && (currentUserId === hoveredNode.authorId || isAdmin) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNodeDelete(hoveredNode);
|
||||
}}
|
||||
className="w-full mt-2 px-3 py-1.5 text-xs text-destructive hover:bg-destructive/10 rounded border border-destructive/20 transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Remove from flow
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PromptConnections({
|
||||
promptId,
|
||||
promptTitle,
|
||||
canEdit,
|
||||
currentUserId,
|
||||
isAdmin,
|
||||
buttonOnly = false,
|
||||
sectionOnly = false,
|
||||
expanded: controlledExpanded,
|
||||
@@ -79,16 +464,29 @@ export function PromptConnections({
|
||||
const [connectionType, setConnectionType] = useState<"previous" | "next">("next");
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Full flow graph state
|
||||
const [flowNodes, setFlowNodes] = useState<FlowGraphNode[]>([]);
|
||||
const [flowEdges, setFlowEdges] = useState<FlowGraphEdge[]>([]);
|
||||
|
||||
const fetchConnections = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/prompts/${promptId}/connections`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Fetch both regular connections and full flow
|
||||
const [connRes, flowRes] = await Promise.all([
|
||||
fetch(`/api/prompts/${promptId}/connections`, { cache: "no-store" }),
|
||||
fetch(`/api/prompts/${promptId}/flow`, { cache: "no-store" }),
|
||||
]);
|
||||
|
||||
if (connRes.ok) {
|
||||
const data = await connRes.json();
|
||||
setOutgoing(data.outgoing || []);
|
||||
setIncoming(data.incoming || []);
|
||||
}
|
||||
|
||||
if (flowRes.ok) {
|
||||
const flowData = await flowRes.json();
|
||||
setFlowNodes(flowData.nodes || []);
|
||||
setFlowEdges(flowData.edges || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch connections:", err);
|
||||
} finally {
|
||||
@@ -100,6 +498,42 @@ export function PromptConnections({
|
||||
fetchConnections();
|
||||
}, [fetchConnections]);
|
||||
|
||||
// Handle removing a node from the flow (deletes all its connections)
|
||||
const handleRemoveFromFlow = useCallback(async (node: FlowGraphNode) => {
|
||||
if (!confirm(`Remove "${node.title}" from this flow? This will delete all connections to/from this prompt.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find all edges connected to this node
|
||||
const edgesToDelete = flowEdges.filter(
|
||||
e => e.source === node.id || e.target === node.id
|
||||
);
|
||||
|
||||
// Delete each connection via API
|
||||
for (const edge of edgesToDelete) {
|
||||
// We need to find the connection ID - fetch connections for the source prompt
|
||||
const res = await fetch(`/api/prompts/${edge.source}/connections`, { cache: "no-store" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const conn = data.outgoing?.find((c: OutgoingConnection) => c.targetId === edge.target);
|
||||
if (conn) {
|
||||
await fetch(`/api/prompts/${edge.source}/connections/${conn.id}`, {
|
||||
method: "DELETE",
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Removed from flow");
|
||||
fetchConnections();
|
||||
} catch (err) {
|
||||
console.error("Failed to remove from flow:", err);
|
||||
toast.error("Failed to remove from flow");
|
||||
}
|
||||
}, [flowEdges, fetchConnections]);
|
||||
|
||||
const handleDeleteConnection = useCallback(async (connectionId: string) => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
@@ -589,6 +1023,7 @@ export function PromptConnections({
|
||||
}
|
||||
|
||||
const hasConnections = outgoing.length > 0 || incoming.length > 0;
|
||||
const hasFullFlow = flowNodes.length > 1 && flowEdges.length > 0;
|
||||
|
||||
// Show to everyone if there are connections, otherwise only show to owners/admins
|
||||
if (!canEdit && !hasConnections) {
|
||||
@@ -670,7 +1105,22 @@ export function PromptConnections({
|
||||
|
||||
{!hasConnections ? (
|
||||
<p className="text-sm text-muted-foreground">{t("noConnections")}</p>
|
||||
) : hasFullFlow ? (
|
||||
/* Full Flow Graph Display with D3 */
|
||||
<div className="w-full space-y-2">
|
||||
<p className="text-xs text-muted-foreground text-center mb-3">{t("fullFlow")}</p>
|
||||
<FlowGraph
|
||||
nodes={flowNodes}
|
||||
edges={flowEdges}
|
||||
currentPromptId={promptId}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
onNodeClick={(node) => router.push(getPromptLink(node))}
|
||||
onNodeDelete={canEdit ? handleRemoveFromFlow : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Original D3 Spider Diagram for simple connections */
|
||||
<div className="w-full">
|
||||
{incoming.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground mb-2 text-center">{t("previousSteps")}</p>
|
||||
@@ -755,6 +1205,19 @@ export function PromptConnections({
|
||||
|
||||
{!hasConnections ? (
|
||||
<p className="text-sm text-muted-foreground">{t("noConnections")}</p>
|
||||
) : hasFullFlow ? (
|
||||
<div className="w-full space-y-2">
|
||||
<p className="text-xs text-muted-foreground text-center mb-3">{t("fullFlow")}</p>
|
||||
<FlowGraph
|
||||
nodes={flowNodes}
|
||||
edges={flowEdges}
|
||||
currentPromptId={promptId}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
onNodeClick={(node) => router.push(getPromptLink(node))}
|
||||
onNodeDelete={canEdit ? handleRemoveFromFlow : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
{incoming.length > 0 && (
|
||||
|
||||
@@ -10,6 +10,8 @@ interface PromptFlowSectionProps {
|
||||
canEdit: boolean;
|
||||
isOwner: boolean;
|
||||
isLoggedIn: boolean;
|
||||
currentUserId?: string;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export function PromptFlowSection({
|
||||
@@ -18,6 +20,8 @@ export function PromptFlowSection({
|
||||
canEdit,
|
||||
isOwner,
|
||||
isLoggedIn,
|
||||
currentUserId,
|
||||
isAdmin,
|
||||
}: PromptFlowSectionProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -29,6 +33,8 @@ export function PromptFlowSection({
|
||||
promptId={promptId}
|
||||
promptTitle={promptTitle}
|
||||
canEdit={canEdit}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
buttonOnly
|
||||
expanded={expanded}
|
||||
onExpandChange={setExpanded}
|
||||
@@ -43,6 +49,8 @@ export function PromptFlowSection({
|
||||
promptId={promptId}
|
||||
promptTitle={promptTitle}
|
||||
canEdit={canEdit}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
sectionOnly
|
||||
expanded={expanded}
|
||||
onExpandChange={setExpanded}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface MasonryProps {
|
||||
default: number;
|
||||
md?: number;
|
||||
lg?: number;
|
||||
xl?: number;
|
||||
};
|
||||
gap?: number;
|
||||
className?: string;
|
||||
@@ -44,7 +45,9 @@ export function Masonry({
|
||||
useEffect(() => {
|
||||
const updateColumns = () => {
|
||||
const width = window.innerWidth;
|
||||
if (width >= 1024 && columnCount.lg) {
|
||||
if (width >= 1280 && columnCount.xl) {
|
||||
setColumns(columnCount.xl);
|
||||
} else if (width >= 1024 && columnCount.lg) {
|
||||
setColumns(columnCount.lg);
|
||||
} else if (width >= 768 && columnCount.md) {
|
||||
setColumns(columnCount.md);
|
||||
@@ -104,7 +107,29 @@ export function Masonry({
|
||||
calculatePositions();
|
||||
}, [columns, isRTL, calculatePositions]);
|
||||
|
||||
// Use ResizeObserver for image loading
|
||||
// Recalculate positions on window resize (container width may change)
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
calculatePositions();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [calculatePositions]);
|
||||
|
||||
// Use ResizeObserver on container for layout changes
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
calculatePositions();
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [calculatePositions]);
|
||||
|
||||
// Use ResizeObserver for image loading and item size changes
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
calculatePositions();
|
||||
|
||||
Reference in New Issue
Block a user