feat(messages): add new translations for 'workflows' in multiple languages

This commit is contained in:
Fatih Kadir Akın
2026-01-23 00:42:26 +03:00
parent 615d5cfde5
commit 0da5e59e67
31 changed files with 1471 additions and 46 deletions
+11 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "已选提示词",
+327
View File
@@ -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();
});
+4 -1
View File
@@ -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 },
},
};
+170
View File
@@ -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 }
);
}
}
+4 -1
View File
@@ -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 },
},
},
}),
+2
View File
@@ -652,6 +652,8 @@ export default async function PromptPage({ params }: PromptPageProps) {
canEdit={canEdit}
isOwner={isOwner}
isLoggedIn={!!session?.user}
currentUserId={session?.user?.id}
isAdmin={isAdmin}
/>
)}
+4 -1
View File
@@ -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) {
+1 -1
View File
@@ -75,7 +75,7 @@ function getCachedSkills(
},
},
_count: {
select: { votes: true, contributors: true },
select: { votes: true, contributors: true, outgoingConnections: true, incomingConnections: true },
},
},
}),
+160
View File
@@ -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>
);
}
+72 -6
View File
@@ -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} />
+29 -4
View File
@@ -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>
+469 -6
View File
@@ -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}
+27 -2
View File
@@ -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();