mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-03-03 00:47:02 +00:00
chore(messages): update language files for prompts and skills
This commit is contained in:
@@ -38,7 +38,8 @@
|
||||
"feed": "الخلاصة",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "الأوامر",
|
||||
"categories": "التصنيفات",
|
||||
"skills": "المهارات",
|
||||
"categories": "الفئات",
|
||||
"tags": "الوسوم",
|
||||
"settings": "الإعدادات",
|
||||
"admin": "الإدارة",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "الأوامر",
|
||||
"create": "إنشاء أمر",
|
||||
"createSkill": "إنشاء مهارة",
|
||||
"skillsDescription": "مهارات الوكيل هي أوامر متعددة الملفات تمنح وكلاء الذكاء الاصطناعي قدرات متخصصة. تتضمن تعليمات وإعدادات وملفات داعمة يمكن استخدامها مع Claude وCursor وWindsurf ومساعدي البرمجة الآخرين.",
|
||||
"createInfo": "هذه المنصة لا تقوم بتشغيل أو تنفيذ الأوامر — إنها مكتبة مجتمعية لمشاركة واكتشاف أوامر الذكاء الاصطناعي. أنشئ أمرك هنا، ويمكن للآخرين نسخه واستخدامه في أدوات الذكاء الاصطناعي المفضلة لديهم مثل ChatGPT و Claude و Gemini أو أي نموذج لغوي آخر. يمكن للمجتمع أيضاً التعليق على أوامرك واقتراح تحسينات من خلال طلبات التغيير.",
|
||||
"hfDataStudio": {
|
||||
"button": "استوديو بيانات HF",
|
||||
@@ -244,7 +247,29 @@
|
||||
"run": "تشغيل",
|
||||
"downloadMarkdown": "تحميل MD",
|
||||
"downloadYaml": "تحميل YAML",
|
||||
"downloadSkillMd": "تحميل SKILL.md",
|
||||
"downloadSkillMd": "تنزيل SKILL.md",
|
||||
"downloadSkill": "تنزيل مهارة",
|
||||
"skillFiles": "ملفات المهارة",
|
||||
"copy": "نسخ",
|
||||
"download": "تحميل",
|
||||
"addFile": "إضافة ملف",
|
||||
"deleteFile": "حذف ملف",
|
||||
"file": "ملف",
|
||||
"files": "ملفات",
|
||||
"addNewFile": "إضافة ملف جديد",
|
||||
"addNewFileDescription": "أدخل اسم الملف مع الامتداد. استخدم / للمجلدات (مثل config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "حذف الملف؟",
|
||||
"deleteFileDescription": "هل أنت متأكد من حذف \"{filename}\"؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"validation": {
|
||||
"filenameEmpty": "اسم الملف لا يمكن أن يكون فارغاً",
|
||||
"filenameInvalidChars": "اسم الملف يحتوي على أحرف غير صالحة",
|
||||
"pathStartEndSlash": "المسار لا يمكن أن يبدأ أو ينتهي بـ /",
|
||||
"pathConsecutiveSlashes": "المسار لا يمكن أن يحتوي على شرطات متتالية",
|
||||
"pathContainsDotDot": "المسار لا يمكن أن يحتوي على ..",
|
||||
"filenameReserved": "SKILL.md موجود بالفعل",
|
||||
"filenameDuplicate": "ملف بهذا الاسم موجود بالفعل",
|
||||
"pathTooLong": "المسار طويل جداً (الحد الأقصى 200 حرف)"
|
||||
},
|
||||
"copyMarkdownUrl": "نسخ رابط MD",
|
||||
"copyYamlUrl": "نسخ رابط YAML",
|
||||
"downloadStarted": "بدأ التحميل",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "إرسال طلب التغيير",
|
||||
"created": "تم إرسال طلب التغيير بنجاح",
|
||||
"status": "الحالة",
|
||||
"pending": "قيد الانتظار",
|
||||
"approved": "مقبول",
|
||||
"rejected": "مرفوض",
|
||||
"approve": "قبول",
|
||||
"reject": "رفض",
|
||||
"reviewNote": "ملاحظة المراجعة",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Lent",
|
||||
"promptmasters": "Promptmasterlər",
|
||||
"prompts": "Promptlar",
|
||||
"skills": "Bacarıqlar",
|
||||
"categories": "Kateqoriyalar",
|
||||
"tags": "Etiketlər",
|
||||
"settings": "Parametrlər",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Promptlar",
|
||||
"create": "Prompt Yarat",
|
||||
"createSkill": "Bacarıq Yarat",
|
||||
"skillsDescription": "Agent Bacarıqları AI agentlərinə xüsusi qabiliyyətlər verən çox fayllı promptlardır. Claude, Cursor, Windsurf və digər AI kodlaşdırma köməkçiləri ilə istifadə edilə bilən təlimatlar, konfiqurasiyalar və dəstəkləyici fayllar daxildir.",
|
||||
"createInfo": "Bu platforma promptları işlətmir — süni intellekt promptlarını paylaşmaq və kəşf etmək üçün icma tərəfindən idarə olunan bir kitabxanadır. Promptunuzu burada yaradın və başqaları onu ChatGPT, Claude, Gemini və ya hər hansı digər LLM kimi üstünlük verdikləri AI alətlərində kopyalayıb istifadə edə bilərlər.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "MD Yüklə",
|
||||
"downloadYaml": "YAML Yüklə",
|
||||
"downloadSkillMd": "SKILL.md Yüklə",
|
||||
"downloadSkill": ".skill Yüklə",
|
||||
"skillFiles": "Bacarıq Faylları",
|
||||
"copy": "Kopyala",
|
||||
"download": "Yüklə",
|
||||
"addFile": "Fayl Əlavə Et",
|
||||
"deleteFile": "Faylı Sil",
|
||||
"file": "fayl",
|
||||
"files": "fayllar",
|
||||
"addNewFile": "Yeni Fayl Əlavə Et",
|
||||
"addNewFileDescription": "Uzantı ilə fayl adını daxil edin. Qovluqlar üçün / istifadə edin (məs., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Fayl Silinsin?",
|
||||
"deleteFileDescription": "\"{filename}\" faylını silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Fayl adı boş ola bilməz",
|
||||
"filenameInvalidChars": "Fayl adında yanlış simvollar var",
|
||||
"pathStartEndSlash": "Yol / ilə başlaya və ya bitə bilməz",
|
||||
"pathConsecutiveSlashes": "Yolda ardıcıl xətt ola bilməz",
|
||||
"pathContainsDotDot": "Yolda .. ola bilməz",
|
||||
"filenameReserved": "SKILL.md artıq mövcuddur",
|
||||
"filenameDuplicate": "Bu adda fayl artıq mövcuddur",
|
||||
"pathTooLong": "Yol çox uzundur (maksimum 200 simvol)"
|
||||
},
|
||||
"copyMarkdownUrl": "MD Linkini Kopyala",
|
||||
"copyYamlUrl": "YAML Linkini Kopyala",
|
||||
"downloadStarted": "Yükləmə başladı",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Dəyişiklik Sorğusu Göndər",
|
||||
"created": "Dəyişiklik sorğusu uğurla göndərildi",
|
||||
"status": "Status",
|
||||
"pending": "Gözləyir",
|
||||
"approved": "Təsdiqləndi",
|
||||
"rejected": "Rədd edildi",
|
||||
"approve": "Təsdiq et",
|
||||
"reject": "Rədd et",
|
||||
"reviewNote": "Nəzərdən Keçirmə Qeydi",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Skills",
|
||||
"categories": "Kategorien",
|
||||
"tags": "Tags",
|
||||
"settings": "Einstellungen",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Prompt erstellen",
|
||||
"createSkill": "Skill erstellen",
|
||||
"skillsDescription": "Agent Skills sind Multi-Datei-Prompts, die KI-Agenten spezialisierte Fähigkeiten verleihen. Sie enthalten Anweisungen, Konfigurationen und unterstützende Dateien, die mit Claude, Cursor, Windsurf und anderen KI-Coding-Assistenten verwendet werden können.",
|
||||
"createInfo": "Diese Plattform führt keine Prompts aus — sie ist eine community-gesteuerte Bibliothek zum Teilen und Entdecken von KI-Prompts. Erstellen Sie hier Ihren Prompt, und andere können ihn kopieren und in ihren bevorzugten KI-Tools wie ChatGPT, Claude, Gemini oder anderen LLMs verwenden. Die Community kann auch Ihre Prompts kommentieren und Verbesserungen durch Änderungsanfragen vorschlagen.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "MD herunterladen",
|
||||
"downloadYaml": "YAML herunterladen",
|
||||
"downloadSkillMd": "SKILL.md herunterladen",
|
||||
"downloadSkill": ".skill herunterladen",
|
||||
"skillFiles": "Skill-Dateien",
|
||||
"copy": "Kopieren",
|
||||
"download": "Herunterladen",
|
||||
"addFile": "Datei hinzufügen",
|
||||
"deleteFile": "Datei löschen",
|
||||
"file": "Datei",
|
||||
"files": "Dateien",
|
||||
"addNewFile": "Neue Datei hinzufügen",
|
||||
"addNewFileDescription": "Geben Sie einen Dateinamen mit Erweiterung ein. Verwenden Sie / für Verzeichnisse (z.B. config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Datei löschen?",
|
||||
"deleteFileDescription": "Sind Sie sicher, dass Sie \"{filename}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Dateiname darf nicht leer sein",
|
||||
"filenameInvalidChars": "Dateiname enthält ungültige Zeichen",
|
||||
"pathStartEndSlash": "Pfad darf nicht mit / beginnen oder enden",
|
||||
"pathConsecutiveSlashes": "Pfad darf keine aufeinanderfolgenden Schrägstriche enthalten",
|
||||
"pathContainsDotDot": "Pfad darf kein .. enthalten",
|
||||
"filenameReserved": "SKILL.md existiert bereits",
|
||||
"filenameDuplicate": "Eine Datei mit diesem Namen existiert bereits",
|
||||
"pathTooLong": "Pfad ist zu lang (max. 200 Zeichen)"
|
||||
},
|
||||
"copyMarkdownUrl": "MD-Link kopieren",
|
||||
"copyYamlUrl": "YAML-Link kopieren",
|
||||
"downloadStarted": "Download gestartet",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Änderungsanfrage absenden",
|
||||
"created": "Änderungsanfrage erfolgreich eingereicht",
|
||||
"status": "Status",
|
||||
"pending": "Ausstehend",
|
||||
"approved": "Genehmigt",
|
||||
"rejected": "Abgelehnt",
|
||||
"approve": "Genehmigen",
|
||||
"reject": "Ablehnen",
|
||||
"reviewNote": "Überprüfungsnotiz",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Ροή",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Δεξιότητες",
|
||||
"categories": "Κατηγορίες",
|
||||
"tags": "Ετικέτες",
|
||||
"settings": "Ρυθμίσεις",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Δημιουργία Prompt",
|
||||
"createSkill": "Δημιουργία Δεξιότητας",
|
||||
"skillsDescription": "Οι Δεξιότητες Πράκτορα είναι prompts πολλαπλών αρχείων που δίνουν στους AI πράκτορες εξειδικευμένες δυνατότητες. Περιλαμβάνουν οδηγίες, ρυθμίσεις και υποστηρικτικά αρχεία που μπορούν να χρησιμοποιηθούν με Claude, Cursor, Windsurf και άλλους βοηθούς προγραμματισμού AI.",
|
||||
"createInfo": "Αυτή η πλατφόρμα δεν εκτελεί prompts — είναι μια κοινοτική βιβλιοθήκη για την κοινοποίηση και ανακάλυψη AI prompts. Δημιουργήστε το prompt σας εδώ και άλλοι μπορούν να το αντιγράψουν και να το χρησιμοποιήσουν στα αγαπημένα τους εργαλεία AI όπως ChatGPT, Claude, Gemini ή οποιοδήποτε άλλο LLM. Η κοινότητα μπορεί επίσης να σχολιάσει τα prompts σας και να προτείνει βελτιώσεις μέσω αιτημάτων αλλαγής.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Λήψη MD",
|
||||
"downloadYaml": "Λήψη YAML",
|
||||
"downloadSkillMd": "Λήψη SKILL.md",
|
||||
"downloadSkill": "Λήψη .skill",
|
||||
"skillFiles": "Αρχεία Δεξιοτήτων",
|
||||
"copy": "Αντιγραφή",
|
||||
"download": "Λήψη",
|
||||
"addFile": "Προσθήκη Αρχείου",
|
||||
"deleteFile": "Διαγραφή Αρχείου",
|
||||
"file": "αρχείο",
|
||||
"files": "αρχεία",
|
||||
"addNewFile": "Προσθήκη Νέου Αρχείου",
|
||||
"addNewFileDescription": "Εισάγετε ένα όνομα αρχείου με επέκταση. Χρησιμοποιήστε / για καταλόγους (π.χ., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Διαγραφή Αρχείου;",
|
||||
"deleteFileDescription": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το \"{filename}\"; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Το όνομα αρχείου δεν μπορεί να είναι κενό",
|
||||
"filenameInvalidChars": "Το όνομα αρχείου περιέχει μη έγκυρους χαρακτήρες",
|
||||
"pathStartEndSlash": "Η διαδρομή δεν μπορεί να ξεκινά ή να τελειώνει με /",
|
||||
"pathConsecutiveSlashes": "Η διαδρομή δεν μπορεί να περιέχει διαδοχικές κάθετους",
|
||||
"pathContainsDotDot": "Η διαδρομή δεν μπορεί να περιέχει ..",
|
||||
"filenameReserved": "Το SKILL.md υπάρχει ήδη",
|
||||
"filenameDuplicate": "Ένα αρχείο με αυτό το όνομα υπάρχει ήδη",
|
||||
"pathTooLong": "Η διαδρομή είναι πολύ μεγάλη (μέγιστο 200 χαρακτήρες)"
|
||||
},
|
||||
"copyMarkdownUrl": "Αντιγραφή συνδέσμου MD",
|
||||
"copyYamlUrl": "Αντιγραφή συνδέσμου YAML",
|
||||
"downloadStarted": "Η λήψη ξεκίνησε",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Υποβολή αίτησης αλλαγής",
|
||||
"created": "Η αίτηση αλλαγής υποβλήθηκε επιτυχώς",
|
||||
"status": "Κατάσταση",
|
||||
"pending": "Εκκρεμεί",
|
||||
"approved": "Εγκρίθηκε",
|
||||
"rejected": "Απορρίφθηκε",
|
||||
"approve": "Έγκριση",
|
||||
"reject": "Απόρριψη",
|
||||
"reviewNote": "Σημείωση αξιολόγησης",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Skills",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"settings": "Settings",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Create Prompt",
|
||||
"createSkill": "Create Skill",
|
||||
"skillsDescription": "Agent Skills are multi-file prompts that give AI agents specialized capabilities. They include instructions, configurations, and supporting files that can be used with Claude, Cursor, Windsurf, and other AI coding assistants.",
|
||||
"createInfo": "This platform doesn't run or execute prompts — it's a community-driven library for sharing and discovering AI prompts. Create your prompt here, and others can copy and use it in their preferred AI tools like ChatGPT, Claude, Gemini, or any other LLM. The community can also comment on your prompts and suggest improvements through change requests.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Download MD",
|
||||
"downloadYaml": "Download YAML",
|
||||
"downloadSkillMd": "Download SKILL.md",
|
||||
"downloadSkill": "Download .skill",
|
||||
"skillFiles": "Skill Files",
|
||||
"copy": "Copy",
|
||||
"download": "Download",
|
||||
"addFile": "Add File",
|
||||
"deleteFile": "Delete File",
|
||||
"file": "file",
|
||||
"files": "files",
|
||||
"addNewFile": "Add New File",
|
||||
"addNewFileDescription": "Enter a filename with extension. Use / for directories (e.g., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Delete File?",
|
||||
"deleteFileDescription": "Are you sure you want to delete \"{filename}\"? This action cannot be undone.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Filename cannot be empty",
|
||||
"filenameInvalidChars": "Filename contains invalid characters",
|
||||
"pathStartEndSlash": "Path cannot start or end with /",
|
||||
"pathConsecutiveSlashes": "Path cannot contain consecutive slashes",
|
||||
"pathContainsDotDot": "Path cannot contain ..",
|
||||
"filenameReserved": "SKILL.md already exists",
|
||||
"filenameDuplicate": "A file with this name already exists",
|
||||
"pathTooLong": "Path is too long (max 200 characters)"
|
||||
},
|
||||
"copyMarkdownUrl": "Copy MD Link",
|
||||
"copyYamlUrl": "Copy YAML Link",
|
||||
"downloadStarted": "Download started",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Submit Change Request",
|
||||
"created": "Change request submitted successfully",
|
||||
"status": "Status",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"reviewNote": "Review Note",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Habilidades",
|
||||
"categories": "Categorías",
|
||||
"tags": "Etiquetas",
|
||||
"settings": "Configuración",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Crear Prompt",
|
||||
"createSkill": "Crear Habilidad",
|
||||
"skillsDescription": "Las Habilidades de Agente son prompts de múltiples archivos que dan a los agentes de IA capacidades especializadas. Incluyen instrucciones, configuraciones y archivos de soporte que se pueden usar con Claude, Cursor, Windsurf y otros asistentes de codificación de IA.",
|
||||
"createInfo": "Esta plataforma no ejecuta prompts — es una biblioteca comunitaria para compartir y descubrir prompts de IA. Crea tu prompt aquí, y otros podrán copiarlo y usarlo en sus herramientas de IA favoritas como ChatGPT, Claude, Gemini o cualquier otro LLM. La comunidad también puede comentar tus prompts y sugerir mejoras a través de solicitudes de cambio.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -245,6 +248,28 @@
|
||||
"downloadMarkdown": "Descargar MD",
|
||||
"downloadYaml": "Descargar YAML",
|
||||
"downloadSkillMd": "Descargar SKILL.md",
|
||||
"downloadSkill": "Descargar .skill",
|
||||
"skillFiles": "Archivos de Habilidad",
|
||||
"copy": "Copiar",
|
||||
"download": "Descargar",
|
||||
"addFile": "Agregar Archivo",
|
||||
"deleteFile": "Eliminar Archivo",
|
||||
"file": "archivo",
|
||||
"files": "archivos",
|
||||
"addNewFile": "Agregar Nuevo Archivo",
|
||||
"addNewFileDescription": "Ingrese un nombre de archivo con extensión. Use / para directorios (ej., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "¿Eliminar Archivo?",
|
||||
"deleteFileDescription": "¿Está seguro de que desea eliminar \"{filename}\"? Esta acción no se puede deshacer.",
|
||||
"validation": {
|
||||
"filenameEmpty": "El nombre del archivo no puede estar vacío",
|
||||
"filenameInvalidChars": "El nombre del archivo contiene caracteres inválidos",
|
||||
"pathStartEndSlash": "La ruta no puede comenzar o terminar con /",
|
||||
"pathConsecutiveSlashes": "La ruta no puede contener barras consecutivas",
|
||||
"pathContainsDotDot": "La ruta no puede contener ..",
|
||||
"filenameReserved": "SKILL.md ya existe",
|
||||
"filenameDuplicate": "Ya existe un archivo con este nombre",
|
||||
"pathTooLong": "La ruta es demasiado larga (máx. 200 caracteres)"
|
||||
},
|
||||
"copyMarkdownUrl": "Copiar enlace MD",
|
||||
"copyYamlUrl": "Copiar enlace YAML",
|
||||
"downloadStarted": "Descarga iniciada",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Enviar Solicitud de Cambio",
|
||||
"created": "Solicitud de cambio enviada exitosamente",
|
||||
"status": "Estado",
|
||||
"pending": "Pendiente",
|
||||
"approved": "Aprobado",
|
||||
"rejected": "Rechazado",
|
||||
"approve": "Aprobar",
|
||||
"reject": "Rechazar",
|
||||
"reviewNote": "Nota de Revisión",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "فید",
|
||||
"promptmasters": "پرامپتمسترها",
|
||||
"prompts": "پرامپتها",
|
||||
"skills": "مهارتها",
|
||||
"categories": "دستهبندیها",
|
||||
"tags": "برچسبها",
|
||||
"settings": "تنظیمات",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "پرامپتها",
|
||||
"create": "ایجاد پرامپت",
|
||||
"createSkill": "ایجاد مهارت",
|
||||
"skillsDescription": "مهارتهای عامل، پرامپتهای چند فایلی هستند که به عاملهای هوش مصنوعی قابلیتهای تخصصی میدهند. شامل دستورالعملها، تنظیمات و فایلهای پشتیبانی هستند که میتوانند با Claude، Cursor، Windsurf و سایر دستیاران برنامهنویسی AI استفاده شوند.",
|
||||
"createInfo": "این پلتفرم پرامپتها را اجرا نمیکند — این یک کتابخانه مبتنی بر جامعه برای اشتراکگذاری و کشف پرامپتهای هوش مصنوعی است. پرامپت خود را اینجا بسازید و دیگران میتوانند آن را در ابزارهای هوش مصنوعی مورد علاقه خود مانند ChatGPT، Claude، Gemini یا هر LLM دیگری کپی و استفاده کنند.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "دانلود MD",
|
||||
"downloadYaml": "دانلود YAML",
|
||||
"downloadSkillMd": "دانلود SKILL.md",
|
||||
"downloadSkill": "دانلود .skill",
|
||||
"skillFiles": "فایلهای مهارت",
|
||||
"copy": "کپی",
|
||||
"download": "دانلود",
|
||||
"addFile": "افزودن فایل",
|
||||
"deleteFile": "حذف فایل",
|
||||
"file": "فایل",
|
||||
"files": "فایلها",
|
||||
"addNewFile": "افزودن فایل جدید",
|
||||
"addNewFileDescription": "نام فایل با پسوند را وارد کنید. از / برای پوشهها استفاده کنید (مثل config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "فایل حذف شود؟",
|
||||
"deleteFileDescription": "آیا مطمئن هستید که میخواهید \"{filename}\" را حذف کنید؟ این عمل قابل بازگشت نیست.",
|
||||
"validation": {
|
||||
"filenameEmpty": "نام فایل نمیتواند خالی باشد",
|
||||
"filenameInvalidChars": "نام فایل شامل کاراکترهای نامعتبر است",
|
||||
"pathStartEndSlash": "مسیر نمیتواند با / شروع یا پایان یابد",
|
||||
"pathConsecutiveSlashes": "مسیر نمیتواند شامل اسلشهای متوالی باشد",
|
||||
"pathContainsDotDot": "مسیر نمیتواند شامل .. باشد",
|
||||
"filenameReserved": "SKILL.md از قبل وجود دارد",
|
||||
"filenameDuplicate": "فایلی با این نام از قبل وجود دارد",
|
||||
"pathTooLong": "مسیر خیلی طولانی است (حداکثر ۲۰۰ کاراکتر)"
|
||||
},
|
||||
"copyMarkdownUrl": "کپی لینک MD",
|
||||
"copyYamlUrl": "کپی لینک YAML",
|
||||
"downloadStarted": "دانلود شروع شد",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "ارسال درخواست تغییر",
|
||||
"created": "درخواست تغییر با موفقیت ارسال شد",
|
||||
"status": "وضعیت",
|
||||
"pending": "در انتظار",
|
||||
"approved": "تأیید شده",
|
||||
"rejected": "رد شده",
|
||||
"approve": "تأیید",
|
||||
"reject": "رد",
|
||||
"reviewNote": "یادداشت بررسی",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Flux",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Compétences",
|
||||
"categories": "Catégories",
|
||||
"tags": "Tags",
|
||||
"settings": "Paramètres",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Créer un Prompt",
|
||||
"createSkill": "Créer une Compétence",
|
||||
"skillsDescription": "Les Compétences d'Agent sont des prompts multi-fichiers qui donnent aux agents IA des capacités spécialisées. Ils comprennent des instructions, des configurations et des fichiers de support qui peuvent être utilisés avec Claude, Cursor, Windsurf et d'autres assistants de codage IA.",
|
||||
"createInfo": "Cette plateforme n'exécute pas les prompts — c'est une bibliothèque communautaire pour partager et découvrir des prompts IA. Créez votre prompt ici, et d'autres pourront le copier et l'utiliser dans leurs outils IA préférés comme ChatGPT, Claude, Gemini ou tout autre LLM. La communauté peut également commenter vos prompts et suggérer des améliorations via des demandes de modification.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Télécharger MD",
|
||||
"downloadYaml": "Télécharger YAML",
|
||||
"downloadSkillMd": "Télécharger SKILL.md",
|
||||
"downloadSkill": "Télécharger .skill",
|
||||
"skillFiles": "Fichiers de Compétence",
|
||||
"copy": "Copier",
|
||||
"download": "Télécharger",
|
||||
"addFile": "Ajouter un Fichier",
|
||||
"deleteFile": "Supprimer le Fichier",
|
||||
"file": "fichier",
|
||||
"files": "fichiers",
|
||||
"addNewFile": "Ajouter un Nouveau Fichier",
|
||||
"addNewFileDescription": "Entrez un nom de fichier avec extension. Utilisez / pour les répertoires (ex., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Supprimer le Fichier ?",
|
||||
"deleteFileDescription": "Êtes-vous sûr de vouloir supprimer \"{filename}\" ? Cette action est irréversible.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Le nom du fichier ne peut pas être vide",
|
||||
"filenameInvalidChars": "Le nom du fichier contient des caractères invalides",
|
||||
"pathStartEndSlash": "Le chemin ne peut pas commencer ou se terminer par /",
|
||||
"pathConsecutiveSlashes": "Le chemin ne peut pas contenir de barres obliques consécutives",
|
||||
"pathContainsDotDot": "Le chemin ne peut pas contenir ..",
|
||||
"filenameReserved": "SKILL.md existe déjà",
|
||||
"filenameDuplicate": "Un fichier avec ce nom existe déjà",
|
||||
"pathTooLong": "Le chemin est trop long (max. 200 caractères)"
|
||||
},
|
||||
"copyMarkdownUrl": "Copier le lien MD",
|
||||
"copyYamlUrl": "Copier le lien YAML",
|
||||
"downloadStarted": "Téléchargement démarré",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Soumettre la Demande de Modification",
|
||||
"created": "Demande de modification soumise avec succès",
|
||||
"status": "Statut",
|
||||
"pending": "En attente",
|
||||
"approved": "Approuvé",
|
||||
"rejected": "Rejeté",
|
||||
"approve": "Approuver",
|
||||
"reject": "Rejeter",
|
||||
"reviewNote": "Note de Révision",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "פיד",
|
||||
"promptmasters": "מאסטרים",
|
||||
"prompts": "פרומפטים",
|
||||
"skills": "מיומנויות",
|
||||
"categories": "קטגוריות",
|
||||
"tags": "תגיות",
|
||||
"settings": "הגדרות",
|
||||
@@ -101,7 +102,9 @@
|
||||
},
|
||||
"prompts": {
|
||||
"title": "פרומפטים",
|
||||
"create": "צור פרומפט",
|
||||
"create": "יצירת פרומפט",
|
||||
"createSkill": "יצירת מיומנות",
|
||||
"skillsDescription": "מיומנויות סוכן הן פרומפטים מרובי קבצים שמעניקים לסוכני AI יכולות מתמחות. הן כוללות הוראות, הגדרות וקבצי תמיכה שניתן להשתמש בהם עם Claude, Cursor, Windsurf ועוזרי קידוד AI אחרים.",
|
||||
"createInfo": "פלטפורמה זו לא מריצה פרומפטים — היא ספרייה קהילתית לשיתוף וגילוי פרומפטים של AI. צור את הפרומפט שלך כאן, ואחרים יוכלו להעתיק ולהשתמש בו בכלי ה-AI המועדפים עליהם כמו ChatGPT, Claude, Gemini או כל LLM אחר. הקהילה יכולה גם להגיב על הפרומפטים שלך ולהציע שיפורים דרך בקשות שינוי.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "הורד MD",
|
||||
"downloadYaml": "הורד YAML",
|
||||
"downloadSkillMd": "הורד SKILL.md",
|
||||
"downloadSkill": "הורד .skill",
|
||||
"skillFiles": "קבצי מיומנות",
|
||||
"copy": "העתק",
|
||||
"download": "הורד",
|
||||
"addFile": "הוסף קובץ",
|
||||
"deleteFile": "מחק קובץ",
|
||||
"file": "קובץ",
|
||||
"files": "קבצים",
|
||||
"addNewFile": "הוסף קובץ חדש",
|
||||
"addNewFileDescription": "הזן שם קובץ עם סיומת. השתמש ב-/ לתיקיות (למשל, config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "למחוק קובץ?",
|
||||
"deleteFileDescription": "האם אתה בטוח שברצונך למחוק את \"{filename}\"? פעולה זו אינה ניתנת לביטול.",
|
||||
"validation": {
|
||||
"filenameEmpty": "שם הקובץ לא יכול להיות ריק",
|
||||
"filenameInvalidChars": "שם הקובץ מכיל תווים לא חוקיים",
|
||||
"pathStartEndSlash": "הנתיב לא יכול להתחיל או להסתיים ב-/",
|
||||
"pathConsecutiveSlashes": "הנתיב לא יכול להכיל לוכסנים עוקבים",
|
||||
"pathContainsDotDot": "הנתיב לא יכול להכיל ..",
|
||||
"filenameReserved": "SKILL.md כבר קיים",
|
||||
"filenameDuplicate": "קובץ עם שם זה כבר קיים",
|
||||
"pathTooLong": "הנתיב ארוך מדי (מקסימום 200 תווים)"
|
||||
},
|
||||
"copyMarkdownUrl": "העתק קישור MD",
|
||||
"copyYamlUrl": "העתק קישור YAML",
|
||||
"downloadStarted": "ההורדה החלה",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "שלח בקשת שינוי",
|
||||
"created": "בקשת השינוי נשלחה בהצלחה",
|
||||
"status": "סטטוס",
|
||||
"pending": "ממתין",
|
||||
"approved": "אושר",
|
||||
"rejected": "נדחה",
|
||||
"approve": "אשר",
|
||||
"reject": "דחה",
|
||||
"reviewNote": "הערת סקירה",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmaster",
|
||||
"prompts": "Prompt",
|
||||
"skills": "Competenze",
|
||||
"categories": "Categorie",
|
||||
"tags": "Tag",
|
||||
"settings": "Impostazioni",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompt",
|
||||
"create": "Crea Prompt",
|
||||
"createSkill": "Crea Competenza",
|
||||
"skillsDescription": "Le Competenze dell'Agente sono prompt multi-file che danno agli agenti AI capacità specializzate. Includono istruzioni, configurazioni e file di supporto che possono essere utilizzati con Claude, Cursor, Windsurf e altri assistenti di codifica AI.",
|
||||
"createInfo": "Questa piattaforma non esegue i prompt — è una libreria comunitaria per condividere e scoprire prompt AI. Crea il tuo prompt qui, e altri potranno copiarlo e usarlo nei loro strumenti AI preferiti come ChatGPT, Claude, Gemini o qualsiasi altro LLM. La community può anche commentare i tuoi prompt e suggerire miglioramenti tramite richieste di modifica.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Scarica MD",
|
||||
"downloadYaml": "Scarica YAML",
|
||||
"downloadSkillMd": "Scarica SKILL.md",
|
||||
"downloadSkill": "Scarica .skill",
|
||||
"skillFiles": "File di Skill",
|
||||
"copy": "Copia",
|
||||
"download": "Scarica",
|
||||
"addFile": "Aggiungi File",
|
||||
"deleteFile": "Elimina File",
|
||||
"file": "file",
|
||||
"files": "file",
|
||||
"addNewFile": "Aggiungi Nuovo File",
|
||||
"addNewFileDescription": "Inserisci un nome file con estensione. Usa / per le directory (es., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Eliminare File?",
|
||||
"deleteFileDescription": "Sei sicuro di voler eliminare \"{filename}\"? Questa azione non può essere annullata.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Il nome del file non può essere vuoto",
|
||||
"filenameInvalidChars": "Il nome del file contiene caratteri non validi",
|
||||
"pathStartEndSlash": "Il percorso non può iniziare o terminare con /",
|
||||
"pathConsecutiveSlashes": "Il percorso non può contenere barre consecutive",
|
||||
"pathContainsDotDot": "Il percorso non può contenere ..",
|
||||
"filenameReserved": "SKILL.md esiste già",
|
||||
"filenameDuplicate": "Un file con questo nome esiste già",
|
||||
"pathTooLong": "Il percorso è troppo lungo (max 200 caratteri)"
|
||||
},
|
||||
"copyMarkdownUrl": "Copia link MD",
|
||||
"copyYamlUrl": "Copia link YAML",
|
||||
"downloadStarted": "Download avviato",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Invia Richiesta di Modifica",
|
||||
"created": "Richiesta di modifica inviata con successo",
|
||||
"status": "Stato",
|
||||
"pending": "In attesa",
|
||||
"approved": "Approvato",
|
||||
"rejected": "Rifiutato",
|
||||
"approve": "Approva",
|
||||
"reject": "Rifiuta",
|
||||
"reviewNote": "Nota di Revisione",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "フィード",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "プロンプト",
|
||||
"skills": "スキル",
|
||||
"categories": "カテゴリー",
|
||||
"tags": "タグ",
|
||||
"settings": "設定",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "プロンプト",
|
||||
"create": "プロンプトを作成",
|
||||
"createSkill": "スキルを作成",
|
||||
"skillsDescription": "エージェントスキルは、AIエージェントに専門的な機能を与えるマルチファイルプロンプトです。Claude、Cursor、Windsurf、その他のAIコーディングアシスタントで使用できる指示、設定、サポートファイルが含まれています。",
|
||||
"createInfo": "このプラットフォームはプロンプトを実行しません — AIプロンプトを共有・発見するためのコミュニティライブラリです。ここでプロンプトを作成すると、他のユーザーがChatGPT、Claude、Geminiなどのお気に入りのAIツールでコピーして使用できます。コミュニティはあなたのプロンプトにコメントしたり、変更リクエストを通じて改善を提案することもできます。",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -245,6 +248,28 @@
|
||||
"downloadMarkdown": "MDをダウンロード",
|
||||
"downloadYaml": "YAMLをダウンロード",
|
||||
"downloadSkillMd": "SKILL.mdをダウンロード",
|
||||
"downloadSkill": ".skillをダウンロード",
|
||||
"skillFiles": "スキルファイル",
|
||||
"copy": "コピー",
|
||||
"download": "ダウンロード",
|
||||
"addFile": "ファイルを追加",
|
||||
"deleteFile": "ファイルを削除",
|
||||
"file": "ファイル",
|
||||
"files": "ファイル",
|
||||
"addNewFile": "新しいファイルを追加",
|
||||
"addNewFileDescription": "拡張子付きのファイル名を入力してください。ディレクトリには / を使用してください(例: config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "ファイルを削除しますか?",
|
||||
"deleteFileDescription": "「{filename}」を削除してもよろしいですか?この操作は取り消せません。",
|
||||
"validation": {
|
||||
"filenameEmpty": "ファイル名は空にできません",
|
||||
"filenameInvalidChars": "ファイル名に無効な文字が含まれています",
|
||||
"pathStartEndSlash": "パスは / で始めたり終わったりできません",
|
||||
"pathConsecutiveSlashes": "パスに連続したスラッシュは使用できません",
|
||||
"pathContainsDotDot": "パスに .. は使用できません",
|
||||
"filenameReserved": "SKILL.md は既に存在します",
|
||||
"filenameDuplicate": "この名前のファイルは既に存在します",
|
||||
"pathTooLong": "パスが長すぎます(最大200文字)"
|
||||
},
|
||||
"copyMarkdownUrl": "MDリンクをコピー",
|
||||
"copyYamlUrl": "YAMLリンクをコピー",
|
||||
"downloadStarted": "ダウンロード開始",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "変更リクエストを送信",
|
||||
"created": "変更リクエストを送信しました",
|
||||
"status": "ステータス",
|
||||
"pending": "保留中",
|
||||
"approved": "承認済み",
|
||||
"rejected": "却下済み",
|
||||
"approve": "承認",
|
||||
"reject": "却下",
|
||||
"reviewNote": "レビューノート",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "피드",
|
||||
"promptmasters": "프롬프트마스터",
|
||||
"prompts": "프롬프트",
|
||||
"skills": "스킬",
|
||||
"categories": "카테고리",
|
||||
"tags": "태그",
|
||||
"settings": "설정",
|
||||
@@ -101,7 +102,9 @@
|
||||
},
|
||||
"prompts": {
|
||||
"title": "프롬프트",
|
||||
"create": "프롬프트 생성",
|
||||
"create": "프롬프트 만들기",
|
||||
"createSkill": "스킬 만들기",
|
||||
"skillsDescription": "에이전트 스킬은 AI 에이전트에게 전문 기능을 제공하는 멀티 파일 프롬프트입니다. Claude, Cursor, Windsurf 및 기타 AI 코딩 어시스턴트와 함께 사용할 수 있는 지침, 구성 및 지원 파일이 포함되어 있습니다.",
|
||||
"createInfo": "이 플랫폼은 프롬프트를 실행하지 않습니다 — AI 프롬프트를 공유하고 발견하는 커뮤니티 라이브러리입니다. 여기서 프롬프트를 만들면 다른 사용자들이 ChatGPT, Claude, Gemini 등 선호하는 AI 도구에서 복사하여 사용할 수 있습니다. 커뮤니티는 프롬프트에 댓글을 달고 변경 요청을 통해 개선 사항을 제안할 수도 있습니다.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF 데이터 스튜디오",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "MD 다운로드",
|
||||
"downloadYaml": "YAML 다운로드",
|
||||
"downloadSkillMd": "SKILL.md 다운로드",
|
||||
"downloadSkill": ".skill 다운로드",
|
||||
"skillFiles": "스킬 파일",
|
||||
"copy": "복사",
|
||||
"download": "다운로드",
|
||||
"addFile": "파일 추가",
|
||||
"deleteFile": "파일 삭제",
|
||||
"file": "파일",
|
||||
"files": "파일",
|
||||
"addNewFile": "새 파일 추가",
|
||||
"addNewFileDescription": "확장자가 포함된 파일명을 입력하세요. 디렉토리는 /를 사용하세요 (예: config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "파일을 삭제하시겠습니까?",
|
||||
"deleteFileDescription": "\"{filename}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
"validation": {
|
||||
"filenameEmpty": "파일명은 비워둘 수 없습니다",
|
||||
"filenameInvalidChars": "파일명에 잘못된 문자가 포함되어 있습니다",
|
||||
"pathStartEndSlash": "경로는 /로 시작하거나 끝날 수 없습니다",
|
||||
"pathConsecutiveSlashes": "경로에 연속된 슬래시를 포함할 수 없습니다",
|
||||
"pathContainsDotDot": "경로에 ..을 포함할 수 없습니다",
|
||||
"filenameReserved": "SKILL.md가 이미 존재합니다",
|
||||
"filenameDuplicate": "이 이름의 파일이 이미 존재합니다",
|
||||
"pathTooLong": "경로가 너무 깁니다 (최대 200자)"
|
||||
},
|
||||
"copyMarkdownUrl": "MD 링크 복사",
|
||||
"copyYamlUrl": "YAML 링크 복사",
|
||||
"downloadStarted": "다운로드 시작",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "변경 요청 제출",
|
||||
"created": "변경 요청이 성공적으로 제출되었습니다",
|
||||
"status": "상태",
|
||||
"pending": "대기 중",
|
||||
"approved": "승인됨",
|
||||
"rejected": "거절됨",
|
||||
"approve": "승인",
|
||||
"reject": "거부",
|
||||
"reviewNote": "검토 메모",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Vaardigheden",
|
||||
"categories": "Categorieën",
|
||||
"tags": "Tags",
|
||||
"settings": "Instellingen",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Prompt aanmaken",
|
||||
"createSkill": "Vaardigheid aanmaken",
|
||||
"skillsDescription": "Agent Vaardigheden zijn multi-bestand prompts die AI-agenten gespecialiseerde mogelijkheden geven. Ze bevatten instructies, configuraties en ondersteunende bestanden die kunnen worden gebruikt met Claude, Cursor, Windsurf en andere AI-coderingsassistenten.",
|
||||
"createInfo": "Dit platform voert geen prompts uit — het is een door de community beheerde bibliotheek voor het delen en ontdekken van AI-prompts. Maak hier je prompt aan, en anderen kunnen het kopiëren en gebruiken in hun favoriete AI-tools zoals ChatGPT, Claude, Gemini of andere LLM's. De community kan ook reageren op je prompts en verbeteringen voorstellen via wijzigingsverzoeken.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -231,7 +234,29 @@
|
||||
"run": "Uitvoeren",
|
||||
"downloadMarkdown": "MD downloaden",
|
||||
"downloadYaml": "YAML downloaden",
|
||||
"downloadSkillMd": "SKILL.md downloaden",
|
||||
"downloadSkillMd": "Download SKILL.md",
|
||||
"downloadSkill": "Download .skill",
|
||||
"skillFiles": "Vaardigheidsbestanden",
|
||||
"copy": "Kopiëren",
|
||||
"download": "Downloaden",
|
||||
"addFile": "Bestand toevoegen",
|
||||
"deleteFile": "Bestand verwijderen",
|
||||
"file": "bestand",
|
||||
"files": "bestanden",
|
||||
"addNewFile": "Nieuw bestand toevoegen",
|
||||
"addNewFileDescription": "Voer een bestandsnaam met extensie in. Gebruik / voor mappen (bijv. config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Bestand verwijderen?",
|
||||
"deleteFileDescription": "Weet je zeker dat je \"{filename}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Bestandsnaam mag niet leeg zijn",
|
||||
"filenameInvalidChars": "Bestandsnaam bevat ongeldige tekens",
|
||||
"pathStartEndSlash": "Pad mag niet beginnen of eindigen met /",
|
||||
"pathConsecutiveSlashes": "Pad mag geen opeenvolgende schuine strepen bevatten",
|
||||
"pathContainsDotDot": "Pad mag geen .. bevatten",
|
||||
"filenameReserved": "SKILL.md bestaat al",
|
||||
"filenameDuplicate": "Een bestand met deze naam bestaat al",
|
||||
"pathTooLong": "Pad is te lang (max 200 tekens)"
|
||||
},
|
||||
"copyMarkdownUrl": "MD-link kopiëren",
|
||||
"copyYamlUrl": "YAML-link kopiëren",
|
||||
"downloadStarted": "Download gestart",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Wijzigingsverzoek indienen",
|
||||
"created": "Wijzigingsverzoek succesvol ingediend",
|
||||
"status": "Status",
|
||||
"pending": "In afwachting",
|
||||
"approved": "Goedgekeurd",
|
||||
"rejected": "Afgewezen",
|
||||
"approve": "Goedkeuren",
|
||||
"reject": "Afwijzen",
|
||||
"reviewNote": "Beoordelingsnotitie",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
"skills": "Habilidades",
|
||||
"categories": "Categorias",
|
||||
"tags": "Tags",
|
||||
"settings": "Configurações",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Prompts",
|
||||
"create": "Criar Prompt",
|
||||
"createSkill": "Criar Habilidade",
|
||||
"skillsDescription": "Habilidades de Agente são prompts multi-arquivo que dão aos agentes de IA capacidades especializadas. Incluem instruções, configurações e arquivos de suporte que podem ser usados com Claude, Cursor, Windsurf e outros assistentes de codificação de IA.",
|
||||
"createInfo": "Esta plataforma não executa prompts — é uma biblioteca comunitária para compartilhar e descobrir prompts de IA. Crie seu prompt aqui, e outros poderão copiá-lo e usá-lo em suas ferramentas de IA favoritas como ChatGPT, Claude, Gemini ou qualquer outro LLM. A comunidade também pode comentar seus prompts e sugerir melhorias através de solicitações de alteração.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Baixar MD",
|
||||
"downloadYaml": "Baixar YAML",
|
||||
"downloadSkillMd": "Baixar SKILL.md",
|
||||
"downloadSkill": "Baixar .skill",
|
||||
"skillFiles": "Arquivos de Habilidade",
|
||||
"copy": "Copiar",
|
||||
"download": "Baixar",
|
||||
"addFile": "Adicionar Arquivo",
|
||||
"deleteFile": "Excluir Arquivo",
|
||||
"file": "arquivo",
|
||||
"files": "arquivos",
|
||||
"addNewFile": "Adicionar Novo Arquivo",
|
||||
"addNewFileDescription": "Digite um nome de arquivo com extensão. Use / para diretórios (ex., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Excluir Arquivo?",
|
||||
"deleteFileDescription": "Tem certeza de que deseja excluir \"{filename}\"? Esta ação não pode ser desfeita.",
|
||||
"validation": {
|
||||
"filenameEmpty": "O nome do arquivo não pode estar vazio",
|
||||
"filenameInvalidChars": "O nome do arquivo contém caracteres inválidos",
|
||||
"pathStartEndSlash": "O caminho não pode começar ou terminar com /",
|
||||
"pathConsecutiveSlashes": "O caminho não pode conter barras consecutivas",
|
||||
"pathContainsDotDot": "O caminho não pode conter ..",
|
||||
"filenameReserved": "SKILL.md já existe",
|
||||
"filenameDuplicate": "Um arquivo com este nome já existe",
|
||||
"pathTooLong": "O caminho é muito longo (máx. 200 caracteres)"
|
||||
},
|
||||
"copyMarkdownUrl": "Copiar link MD",
|
||||
"copyYamlUrl": "Copiar link YAML",
|
||||
"downloadStarted": "Download iniciado",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Enviar Solicitação de Alteração",
|
||||
"created": "Solicitação de alteração enviada com sucesso",
|
||||
"status": "Status",
|
||||
"pending": "Pendente",
|
||||
"approved": "Aprovado",
|
||||
"rejected": "Rejeitado",
|
||||
"approve": "Aprovar",
|
||||
"reject": "Rejeitar",
|
||||
"reviewNote": "Nota de Revisão",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Лента",
|
||||
"promptmasters": "Мастера промптов",
|
||||
"prompts": "Промпты",
|
||||
"skills": "Навыки",
|
||||
"categories": "Категории",
|
||||
"tags": "Теги",
|
||||
"settings": "Настройки",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Промпты",
|
||||
"create": "Создать промпт",
|
||||
"createSkill": "Создать навык",
|
||||
"skillsDescription": "Навыки агента — это многофайловые промпты, которые дают AI-агентам специализированные возможности. Они включают инструкции, конфигурации и вспомогательные файлы, которые можно использовать с Claude, Cursor, Windsurf и другими AI-помощниками для программирования.",
|
||||
"createInfo": "Эта платформа не выполняет промпты — это библиотека сообщества для обмена и поиска AI-промптов. Создайте свой промпт здесь, и другие смогут скопировать и использовать его в своих любимых AI-инструментах, таких как ChatGPT, Claude, Gemini или любой другой LLM. Сообщество также может комментировать ваши промпты и предлагать улучшения через запросы на изменение.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Data Studio",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "Скачать MD",
|
||||
"downloadYaml": "Скачать YAML",
|
||||
"downloadSkillMd": "Скачать SKILL.md",
|
||||
"downloadSkill": "Скачать .skill",
|
||||
"skillFiles": "Файлы навыков",
|
||||
"copy": "Копировать",
|
||||
"download": "Скачать",
|
||||
"addFile": "Добавить файл",
|
||||
"deleteFile": "Удалить файл",
|
||||
"file": "файл",
|
||||
"files": "файлы",
|
||||
"addNewFile": "Добавить новый файл",
|
||||
"addNewFileDescription": "Введите имя файла с расширением. Используйте / для директорий (например, config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Удалить файл?",
|
||||
"deleteFileDescription": "Вы уверены, что хотите удалить \"{filename}\"? Это действие нельзя отменить.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Имя файла не может быть пустым",
|
||||
"filenameInvalidChars": "Имя файла содержит недопустимые символы",
|
||||
"pathStartEndSlash": "Путь не может начинаться или заканчиваться на /",
|
||||
"pathConsecutiveSlashes": "Путь не может содержать последовательные косые черты",
|
||||
"pathContainsDotDot": "Путь не может содержать ..",
|
||||
"filenameReserved": "SKILL.md уже существует",
|
||||
"filenameDuplicate": "Файл с таким именем уже существует",
|
||||
"pathTooLong": "Путь слишком длинный (макс. 200 символов)"
|
||||
},
|
||||
"copyMarkdownUrl": "Копировать ссылку MD",
|
||||
"copyYamlUrl": "Копировать ссылку YAML",
|
||||
"downloadStarted": "Загрузка началась",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Отправить запрос на изменение",
|
||||
"created": "Запрос на изменение успешно отправлен",
|
||||
"status": "Статус",
|
||||
"pending": "На рассмотрении",
|
||||
"approved": "Одобрено",
|
||||
"rejected": "Отклонено",
|
||||
"approve": "Одобрить",
|
||||
"reject": "Отклонить",
|
||||
"reviewNote": "Заметка к рассмотрению",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "Akış",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Promptlar",
|
||||
"skills": "Yetenekler",
|
||||
"categories": "Kategoriler",
|
||||
"tags": "Etiketler",
|
||||
"settings": "Ayarlar",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "Promptlar",
|
||||
"create": "Prompt Oluştur",
|
||||
"createSkill": "Yetenek Oluştur",
|
||||
"skillsDescription": "Ajan Yetenekleri, AI ajanlara uzmanlaşmış yetenekler veren çok dosyalı promptlardır. Claude, Cursor, Windsurf ve diğer AI kodlama asistanlarıyla kullanılabilecek talimatlar, yapılandırmalar ve destek dosyaları içerirler.",
|
||||
"createInfo": "Bu platform promptları çalıştırmaz — AI promptlarını paylaşmak ve keşfetmek için topluluk odaklı bir kütüphanedir. Promptunuzu burada oluşturun, diğerleri ChatGPT, Claude, Gemini veya başka herhangi bir LLM gibi tercih ettikleri AI araçlarında kopyalayıp kullanabilir. Topluluk ayrıca promptlarınıza yorum yapabilir ve değişiklik istekleri aracılığıyla iyileştirmeler önerebilir.",
|
||||
"hfDataStudio": {
|
||||
"button": "HF Veri Stüdyosu",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "MD İndir",
|
||||
"downloadYaml": "YAML İndir",
|
||||
"downloadSkillMd": "SKILL.md İndir",
|
||||
"downloadSkill": ".skill İndir",
|
||||
"skillFiles": "Yetenek Dosyaları",
|
||||
"copy": "Kopyala",
|
||||
"download": "İndir",
|
||||
"addFile": "Dosya Ekle",
|
||||
"deleteFile": "Dosyayı Sil",
|
||||
"file": "dosya",
|
||||
"files": "dosya",
|
||||
"addNewFile": "Yeni Dosya Ekle",
|
||||
"addNewFileDescription": "Uzantılı bir dosya adı girin. Dizinler için / kullanın (örn., config.json, src/utils.ts)",
|
||||
"deleteFileConfirm": "Dosya Silinsin mi?",
|
||||
"deleteFileDescription": "\"{filename}\" dosyasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"validation": {
|
||||
"filenameEmpty": "Dosya adı boş olamaz",
|
||||
"filenameInvalidChars": "Dosya adı geçersiz karakterler içeriyor",
|
||||
"pathStartEndSlash": "Yol / ile başlayamaz veya bitemez",
|
||||
"pathConsecutiveSlashes": "Yol ardışık eğik çizgi içeremez",
|
||||
"pathContainsDotDot": "Yol .. içeremez",
|
||||
"filenameReserved": "SKILL.md zaten mevcut",
|
||||
"filenameDuplicate": "Bu isimde bir dosya zaten mevcut",
|
||||
"pathTooLong": "Yol çok uzun (maks. 200 karakter)"
|
||||
},
|
||||
"copyMarkdownUrl": "MD Linkini Kopyala",
|
||||
"copyYamlUrl": "YAML Linkini Kopyala",
|
||||
"downloadStarted": "İndirme başladı",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "Değişiklik İsteği Gönder",
|
||||
"created": "Değişiklik isteği başarıyla gönderildi",
|
||||
"status": "Durum",
|
||||
"pending": "Beklemede",
|
||||
"approved": "Onaylandı",
|
||||
"rejected": "Reddedildi",
|
||||
"approve": "Onayla",
|
||||
"reject": "Reddet",
|
||||
"reviewNote": "İnceleme Notu",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"feed": "动态",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "提示词",
|
||||
"skills": "技能",
|
||||
"categories": "分类",
|
||||
"tags": "标签",
|
||||
"settings": "设置",
|
||||
@@ -102,6 +103,8 @@
|
||||
"prompts": {
|
||||
"title": "提示词",
|
||||
"create": "创建提示词",
|
||||
"createSkill": "创建技能",
|
||||
"skillsDescription": "智能体技能是多文件提示词,可为AI智能体提供专业能力。它们包含指令、配置和支持文件,可与Claude、Cursor、Windsurf及其他AI编程助手配合使用。",
|
||||
"createInfo": "本平台不运行或执行提示词——这是一个社区驱动的AI提示词分享和发现库。在这里创建您的提示词,其他人可以复制并在他们喜欢的AI工具中使用,如ChatGPT、Claude、Gemini或任何其他LLM。社区还可以评论您的提示词,并通过变更请求提出改进建议。",
|
||||
"hfDataStudio": {
|
||||
"button": "HF 数据工作室",
|
||||
@@ -232,6 +235,28 @@
|
||||
"downloadMarkdown": "下载 MD",
|
||||
"downloadYaml": "下载 YAML",
|
||||
"downloadSkillMd": "下载 SKILL.md",
|
||||
"downloadSkill": "下载 .skill",
|
||||
"skillFiles": "技能文件",
|
||||
"copy": "复制",
|
||||
"download": "下载",
|
||||
"addFile": "添加文件",
|
||||
"deleteFile": "删除文件",
|
||||
"file": "文件",
|
||||
"files": "文件",
|
||||
"addNewFile": "添加新文件",
|
||||
"addNewFileDescription": "输入带扩展名的文件名。使用 / 表示目录(例如,config.json、src/utils.ts)",
|
||||
"deleteFileConfirm": "删除文件?",
|
||||
"deleteFileDescription": "确定要删除 \"{filename}\" 吗?此操作无法撤销。",
|
||||
"validation": {
|
||||
"filenameEmpty": "文件名不能为空",
|
||||
"filenameInvalidChars": "文件名包含无效字符",
|
||||
"pathStartEndSlash": "路径不能以 / 开头或结尾",
|
||||
"pathConsecutiveSlashes": "路径不能包含连续的斜杠",
|
||||
"pathContainsDotDot": "路径不能包含 ..",
|
||||
"filenameReserved": "SKILL.md 已存在",
|
||||
"filenameDuplicate": "此名称的文件已存在",
|
||||
"pathTooLong": "路径太长(最多200个字符)"
|
||||
},
|
||||
"copyMarkdownUrl": "复制 MD 链接",
|
||||
"copyYamlUrl": "复制 YAML 链接",
|
||||
"downloadStarted": "开始下载",
|
||||
@@ -366,6 +391,9 @@
|
||||
"submit": "提交变更请求",
|
||||
"created": "变更请求提交成功",
|
||||
"status": "状态",
|
||||
"pending": "待处理",
|
||||
"approved": "已批准",
|
||||
"rejected": "已拒绝",
|
||||
"approve": "批准",
|
||||
"reject": "拒绝",
|
||||
"reviewNote": "审核备注",
|
||||
|
||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -43,6 +43,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next": "^16.0.10",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
@@ -10224,6 +10225,12 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
@@ -12942,6 +12949,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -13847,6 +13860,18 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -13891,6 +13916,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
@@ -16574,6 +16608,12 @@
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -17011,6 +17051,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
@@ -17349,6 +17395,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
@@ -18030,6 +18103,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -18360,6 +18439,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next": "^16.0.10",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
|
||||
270
scripts/seed-skills.ts
Normal file
270
scripts/seed-skills.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Seed script to import skills from Anthropic's skills repository
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/seed-skills.ts [skill-name]
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts/seed-skills.ts pdf
|
||||
* npx tsx scripts/seed-skills.ts --all
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// File separator uses ASCII control characters (injection-proof):
|
||||
// \x1F (Unit Separator, ASCII 31) marks start
|
||||
// \x1E (Record Separator, ASCII 30) marks end
|
||||
const FILE_SEPARATOR = (filename: string) => `\x1FFILE:${filename}\x1E`;
|
||||
|
||||
interface SkillMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
license?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from SKILL.md
|
||||
*/
|
||||
function parseFrontmatter(content: string): { metadata: SkillMetadata; body: string } {
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
|
||||
if (!frontmatterMatch) {
|
||||
return {
|
||||
metadata: { name: "Unknown", description: "" },
|
||||
body: content,
|
||||
};
|
||||
}
|
||||
|
||||
const [, frontmatter, body] = frontmatterMatch;
|
||||
const metadata: SkillMetadata = { name: "Unknown", description: "" };
|
||||
|
||||
// Simple YAML parsing for frontmatter
|
||||
frontmatter.split("\n").forEach((line) => {
|
||||
const match = line.match(/^(\w+):\s*(.+)$/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
if (key === "name") metadata.name = value.trim();
|
||||
if (key === "description") metadata.description = value.trim();
|
||||
if (key === "license") metadata.license = value.trim();
|
||||
}
|
||||
});
|
||||
|
||||
return { metadata, body };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively read all files from a directory
|
||||
*/
|
||||
function readSkillFiles(skillDir: string, basePath: string = ""): Array<{ path: string; content: string }> {
|
||||
const files: Array<{ path: string; content: string }> = [];
|
||||
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(skillDir, entry.name);
|
||||
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...readSkillFiles(fullPath, relativePath));
|
||||
} else if (entry.isFile()) {
|
||||
// Skip binary files and hidden files
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(fullPath, "utf-8");
|
||||
files.push({ path: relativePath, content });
|
||||
} catch (e) {
|
||||
console.warn(` Skipping binary file: ${relativePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize skill files into multi-file format
|
||||
*/
|
||||
function serializeSkillFiles(files: Array<{ path: string; content: string }>): string {
|
||||
// SKILL.md should be first
|
||||
const skillMd = files.find((f) => f.path === "SKILL.md");
|
||||
const otherFiles = files.filter((f) => f.path !== "SKILL.md");
|
||||
|
||||
if (!skillMd) {
|
||||
throw new Error("SKILL.md not found");
|
||||
}
|
||||
|
||||
let result = skillMd.content;
|
||||
|
||||
for (const file of otherFiles) {
|
||||
result += `\n${FILE_SEPARATOR(file.path)}\n${file.content}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single skill into the database
|
||||
*/
|
||||
async function importSkill(skillDir: string, authorId: string): Promise<void> {
|
||||
const skillName = path.basename(skillDir);
|
||||
console.log(`\nImporting skill: ${skillName}`);
|
||||
|
||||
// Read all files
|
||||
const files = readSkillFiles(skillDir);
|
||||
console.log(` Found ${files.length} files`);
|
||||
|
||||
// Find and parse SKILL.md
|
||||
const skillMdFile = files.find((f) => f.path === "SKILL.md");
|
||||
if (!skillMdFile) {
|
||||
console.error(` ERROR: SKILL.md not found in ${skillDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { metadata } = parseFrontmatter(skillMdFile.content);
|
||||
console.log(` Name: ${metadata.name}`);
|
||||
console.log(` Description: ${metadata.description.substring(0, 80)}...`);
|
||||
|
||||
// Serialize all files
|
||||
const content = serializeSkillFiles(files);
|
||||
|
||||
// Check if skill already exists
|
||||
const existing = await prisma.prompt.findFirst({
|
||||
where: {
|
||||
title: metadata.name,
|
||||
type: "SKILL",
|
||||
authorId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(` Skill "${metadata.name}" already exists, updating...`);
|
||||
await prisma.prompt.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
content,
|
||||
description: metadata.description,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create new skill
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
title: metadata.name,
|
||||
description: metadata.description,
|
||||
content,
|
||||
type: "SKILL",
|
||||
authorId,
|
||||
isPrivate: false,
|
||||
},
|
||||
});
|
||||
console.log(` Created skill: ${metadata.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log("Usage:");
|
||||
console.log(" npx tsx scripts/seed-skills.ts <skill-name> - Import a specific skill");
|
||||
console.log(" npx tsx scripts/seed-skills.ts --all - Import all skills");
|
||||
console.log(" npx tsx scripts/seed-skills.ts --list - List available skills");
|
||||
console.log("\nAvailable skills:");
|
||||
|
||||
const skillsDir = "/tmp/anthropic-skills/skills";
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
skills.forEach((s) => console.log(` - ${s}`));
|
||||
} else {
|
||||
console.log(" (Skills repo not found. Clone it first with: git clone https://github.com/anthropics/skills.git /tmp/anthropic-skills)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create admin user for importing
|
||||
let author = await prisma.user.findFirst({
|
||||
where: { role: "ADMIN" },
|
||||
});
|
||||
|
||||
if (!author) {
|
||||
console.log("No admin user found. Creating system user...");
|
||||
author = await prisma.user.create({
|
||||
data: {
|
||||
email: "system@prompts.chat",
|
||||
username: "system",
|
||||
name: "System",
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Using author: ${author.username} (${author.id})`);
|
||||
|
||||
const skillsBaseDir = "/tmp/anthropic-skills/skills";
|
||||
|
||||
if (!fs.existsSync(skillsBaseDir)) {
|
||||
console.error("Skills directory not found. Please clone the repo first:");
|
||||
console.error(" git clone https://github.com/anthropics/skills.git /tmp/anthropic-skills");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[0] === "--all") {
|
||||
// Import all skills
|
||||
const skillDirs = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => path.join(skillsBaseDir, d.name));
|
||||
|
||||
console.log(`Found ${skillDirs.length} skills to import`);
|
||||
|
||||
for (const skillDir of skillDirs) {
|
||||
try {
|
||||
await importSkill(skillDir, author.id);
|
||||
} catch (e) {
|
||||
console.error(` ERROR importing ${path.basename(skillDir)}:`, e);
|
||||
}
|
||||
}
|
||||
} else if (args[0] === "--list") {
|
||||
// List available skills
|
||||
const skills = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
console.log("Available skills:");
|
||||
skills.forEach((s) => console.log(` - ${s}`));
|
||||
} else {
|
||||
// Import specific skill
|
||||
const skillDir = path.join(skillsBaseDir, args[0]);
|
||||
|
||||
if (!fs.existsSync(skillDir)) {
|
||||
console.error(`Skill not found: ${args[0]}`);
|
||||
console.log("\nAvailable skills:");
|
||||
const skills = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
skills.forEach((s) => console.log(` - ${s}`));
|
||||
return;
|
||||
}
|
||||
|
||||
await importSkill(skillDir, author.id);
|
||||
}
|
||||
|
||||
console.log("\nDone!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
78
src/app/api/prompts/[id]/skill/route.ts
Normal file
78
src/app/api/prompts/[id]/skill/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { parseSkillFiles } from "@/lib/skill-files";
|
||||
import JSZip from "jszip";
|
||||
|
||||
/**
|
||||
* Extracts the prompt ID from a URL parameter
|
||||
* Supports formats: "abc123", "abc123_some-slug"
|
||||
*/
|
||||
function parseIdParam(idParam: string): string {
|
||||
let param = idParam;
|
||||
|
||||
// Remove .skill extension if present
|
||||
if (param.endsWith(".skill")) {
|
||||
param = param.slice(0, -".skill".length);
|
||||
}
|
||||
|
||||
// If the param contains an underscore, extract the ID (everything before first underscore)
|
||||
const underscoreIndex = param.indexOf("_");
|
||||
if (underscoreIndex !== -1) {
|
||||
param = param.substring(0, underscoreIndex);
|
||||
}
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id: idParam } = await params;
|
||||
const id = parseIdParam(idParam);
|
||||
|
||||
// Fetch the skill
|
||||
const prompt = await db.prompt.findFirst({
|
||||
where: { id, deletedAt: null, isPrivate: false, type: "SKILL" },
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
content: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return new NextResponse("Skill not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Parse the skill files
|
||||
const files = parseSkillFiles(prompt.content);
|
||||
|
||||
// Create a zip file
|
||||
const zip = new JSZip();
|
||||
|
||||
// Add each file to the zip
|
||||
for (const file of files) {
|
||||
zip.file(file.filename, file.content);
|
||||
}
|
||||
|
||||
// Generate the zip content
|
||||
const zipContent = await zip.generateAsync({
|
||||
type: "nodebuffer",
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 9 },
|
||||
});
|
||||
|
||||
// Generate filename
|
||||
const slug = prompt.slug || prompt.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
const filename = `${slug}.skill`;
|
||||
|
||||
return new NextResponse(zipContent, {
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { headers } from "next/headers";
|
||||
import { Code, Zap, Terminal, Search, Box, Key, Save, Sparkles } from "lucide-react";
|
||||
import { Code, Zap, Terminal, Search, Box, Key, Save, Sparkles, Cpu, FilePlus, FileX } from "lucide-react";
|
||||
import { ImprovePromptDemo } from "@/components/api/improve-prompt-demo";
|
||||
import {
|
||||
Table,
|
||||
@@ -479,6 +479,234 @@ curl -X POST ${baseUrl}/api/mcp \\
|
||||
"outputFormat": "text"
|
||||
}
|
||||
}
|
||||
}'`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* save_skill Tool */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4" />
|
||||
save_skill
|
||||
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">Requires Auth</span>
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Save a new Agent Skill to your account. Skills are multi-file prompts that can include SKILL.md (required),
|
||||
reference docs, scripts, and configuration files. Perfect for creating comprehensive coding agent skills.
|
||||
</p>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[140px]">Parameter</TableHead>
|
||||
<TableHead className="w-[100px]">Type</TableHead>
|
||||
<TableHead className="w-[80px]">Required</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">title</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">Title of the skill (max 200 chars)</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">files</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">array</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
Array of {`{filename, content}`}. Must include <code className="text-xs">SKILL.md</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">description</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">No</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">Optional description (max 500 chars)</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">tags</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string[]</TableCell>
|
||||
<TableCell className="text-xs">No</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">Array of tag names (max 10)</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">category</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">No</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">Category slug</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">isPrivate</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">boolean</TableCell>
|
||||
<TableCell className="text-xs">No</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">Override default privacy setting</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-lg p-4 font-mono text-sm overflow-x-auto">
|
||||
<pre>{`# Save a skill via MCP
|
||||
curl -X POST ${baseUrl}/api/mcp \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "PROMPTS_API_KEY: pchat_your_api_key_here" \\
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "save_skill",
|
||||
"arguments": {
|
||||
"title": "PDF Processing Skill",
|
||||
"description": "Comprehensive PDF manipulation toolkit",
|
||||
"files": [
|
||||
{"filename": "SKILL.md", "content": "# PDF Processing\\n\\nThis skill helps with PDF manipulation..."},
|
||||
{"filename": "reference.md", "content": "# API Reference\\n\\n..."},
|
||||
{"filename": "scripts/extract.py", "content": "import pypdf\\n..."}
|
||||
],
|
||||
"tags": ["pdf", "documents"]
|
||||
}
|
||||
}
|
||||
}'`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* add_file_to_skill Tool */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<FilePlus className="h-4 w-4" />
|
||||
add_file_to_skill
|
||||
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">Requires Auth</span>
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Add a new file to an existing Agent Skill. Use this to add reference docs, scripts, or configuration files.
|
||||
</p>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[140px]">Parameter</TableHead>
|
||||
<TableHead className="w-[100px]">Type</TableHead>
|
||||
<TableHead className="w-[80px]">Required</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">skillId</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">ID of the skill to add the file to</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">filename</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
File path (e.g., <code className="text-xs">reference.md</code>, <code className="text-xs">scripts/helper.py</code>)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">content</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">File content</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* remove_file_from_skill Tool */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<FileX className="h-4 w-4" />
|
||||
remove_file_from_skill
|
||||
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">Requires Auth</span>
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Remove a file from an existing Agent Skill. Cannot remove SKILL.md as it is required.
|
||||
</p>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[140px]">Parameter</TableHead>
|
||||
<TableHead className="w-[100px]">Type</TableHead>
|
||||
<TableHead className="w-[80px]">Required</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">skillId</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">ID of the skill to remove the file from</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">filename</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">File path to remove (cannot be SKILL.md)</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* get_skill Tool */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4" />
|
||||
get_skill
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Get an Agent Skill by ID, including all its files (SKILL.md, reference docs, scripts, etc.).
|
||||
Returns skill metadata and file contents. Public skills are accessible without authentication;
|
||||
private skills require API key authentication.
|
||||
</p>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[140px]">Parameter</TableHead>
|
||||
<TableHead className="w-[100px]">Type</TableHead>
|
||||
<TableHead className="w-[80px]">Required</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">id</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">string</TableCell>
|
||||
<TableCell className="text-xs">Yes</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">The ID of the skill to retrieve</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-lg p-4 font-mono text-sm overflow-x-auto">
|
||||
<pre>{`# Get a skill via MCP
|
||||
curl -X POST ${baseUrl}/api/mcp \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "get_skill",
|
||||
"arguments": {
|
||||
"id": "skill_id_here"
|
||||
}
|
||||
}
|
||||
}'`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { DiffView } from "@/components/ui/diff-view";
|
||||
import { SkillDiffViewer } from "@/components/prompts/skill-diff-viewer";
|
||||
import { ChangeRequestActions } from "@/components/prompts/change-request-actions";
|
||||
import { ReopenChangeRequestButton } from "@/components/prompts/reopen-change-request-button";
|
||||
import { DismissChangeRequestButton } from "@/components/prompts/dismiss-change-request-button";
|
||||
@@ -52,6 +53,7 @@ export default async function ChangeRequestPage({ params }: ChangeRequestPagePro
|
||||
title: true,
|
||||
content: true,
|
||||
authorId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -151,10 +153,17 @@ export default async function ChangeRequestPage({ params }: ChangeRequestPagePro
|
||||
{/* Content diff */}
|
||||
<div className="mb-6">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">{t("contentChanges")}</p>
|
||||
<DiffView
|
||||
original={changeRequest.originalContent}
|
||||
modified={changeRequest.proposedContent}
|
||||
/>
|
||||
{changeRequest.prompt.type === "SKILL" ? (
|
||||
<SkillDiffViewer
|
||||
original={changeRequest.originalContent}
|
||||
modified={changeRequest.proposedContent}
|
||||
/>
|
||||
) : (
|
||||
<DiffView
|
||||
original={changeRequest.originalContent}
|
||||
modified={changeRequest.proposedContent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Review note (if exists) */}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { InteractivePromptContent } from "@/components/prompts/interactive-prompt-content";
|
||||
import { SkillViewer } from "@/components/prompts/skill-viewer";
|
||||
import { UpvoteButton } from "@/components/prompts/upvote-button";
|
||||
import { AddVersionForm } from "@/components/prompts/add-version-form";
|
||||
import { DeleteVersionButton } from "@/components/prompts/delete-version-button";
|
||||
@@ -548,7 +549,13 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{prompt.structuredFormat ? (
|
||||
{prompt.type === "SKILL" ? (
|
||||
<SkillViewer
|
||||
content={prompt.content}
|
||||
promptId={prompt.id}
|
||||
promptSlug={prompt.slug ?? undefined}
|
||||
/>
|
||||
) : prompt.structuredFormat ? (
|
||||
<InteractivePromptContent
|
||||
content={prompt.content}
|
||||
isStructured={true}
|
||||
@@ -637,14 +644,16 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report & Prompt Flow */}
|
||||
<PromptFlowSection
|
||||
promptId={prompt.id}
|
||||
promptTitle={prompt.title}
|
||||
canEdit={canEdit}
|
||||
isOwner={isOwner}
|
||||
isLoggedIn={!!session?.user}
|
||||
/>
|
||||
{/* Report & Prompt Flow - hide for SKILL type */}
|
||||
{prompt.type !== "SKILL" && (
|
||||
<PromptFlowSection
|
||||
promptId={prompt.id}
|
||||
promptTitle={prompt.title}
|
||||
canEdit={canEdit}
|
||||
isOwner={isOwner}
|
||||
isLoggedIn={!!session?.user}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Related Prompts */}
|
||||
{relatedPrompts.length > 0 && (
|
||||
|
||||
159
src/app/skills/page.tsx
Normal file
159
src/app/skills/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InfinitePromptList } from "@/components/prompts/infinite-prompt-list";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Skills",
|
||||
description: "Browse and discover AI agent skills",
|
||||
};
|
||||
|
||||
// Query for skills list (cached)
|
||||
function getCachedSkills(
|
||||
// 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> = {
|
||||
type: "SKILL",
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (searchQuery) {
|
||||
where.OR = [
|
||||
{ title: { contains: searchQuery, mode: "insensitive" } },
|
||||
{ content: { contains: searchQuery, mode: "insensitive" } },
|
||||
{ description: { contains: searchQuery, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
const [skillsRaw, 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 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.prompt.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
skills: skillsRaw.map((p: any) => ({
|
||||
...p,
|
||||
voteCount: p._count.votes,
|
||||
contributorCount: p._count.contributors,
|
||||
contributors: p.contributors,
|
||||
})),
|
||||
total: totalCount,
|
||||
};
|
||||
},
|
||||
["skills", cacheKey],
|
||||
{ tags: ["prompts"] }
|
||||
)();
|
||||
}
|
||||
|
||||
interface SkillsPageProps {
|
||||
searchParams: Promise<{
|
||||
q?: string;
|
||||
sort?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function SkillsPage({ searchParams }: SkillsPageProps) {
|
||||
const t = await getTranslations("prompts");
|
||||
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 getCachedSkills(orderBy, perPage, params.q);
|
||||
const skills = result.skills;
|
||||
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("skills")}</h1>
|
||||
<span className="text-xs text-muted-foreground">{tSearch("found", { count: total })}</span>
|
||||
</div>
|
||||
<Button size="sm" className="h-8 text-xs w-full sm:w-auto" asChild>
|
||||
<Link href="/prompts/new?type=SKILL">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
{t("createSkill")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{t("skillsDescription")}
|
||||
</p>
|
||||
|
||||
<InfinitePromptList
|
||||
initialPrompts={skills}
|
||||
initialTotal={total}
|
||||
filters={{
|
||||
q: params.q,
|
||||
type: "SKILL",
|
||||
sort: params.sort,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -199,6 +199,13 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
>
|
||||
{t("nav.prompts")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/skills"
|
||||
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.skills")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
@@ -347,6 +354,12 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
>
|
||||
{t("nav.prompts")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/skills"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.skills")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
|
||||
@@ -12,6 +12,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { DiffView } from "@/components/ui/diff-view";
|
||||
import { CodeEditor } from "@/components/ui/code-editor";
|
||||
import { VariableToolbar } from "@/components/prompts/variable-toolbar";
|
||||
import { SkillEditor } from "@/components/prompts/skill-editor";
|
||||
import { SkillDiffViewer } from "@/components/prompts/skill-diff-viewer";
|
||||
import { toast } from "sonner";
|
||||
import { analyticsPrompt } from "@/lib/analytics";
|
||||
|
||||
@@ -25,6 +27,7 @@ interface ChangeRequestFormProps {
|
||||
|
||||
export function ChangeRequestForm({ promptId, currentContent, currentTitle, promptType, structuredFormat }: ChangeRequestFormProps) {
|
||||
const isStructured = promptType === "STRUCTURED";
|
||||
const isSkill = promptType === "SKILL";
|
||||
const router = useRouter();
|
||||
const t = useTranslations("changeRequests");
|
||||
const tCommon = useTranslations("common");
|
||||
@@ -130,32 +133,41 @@ export function ChangeRequestForm({ promptId, currentContent, currentTitle, prom
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="edit" className="mt-0">
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<VariableToolbar onInsert={handleInsertVariable} />
|
||||
{isStructured ? (
|
||||
<CodeEditor
|
||||
value={proposedContent}
|
||||
onChange={setProposedContent}
|
||||
language={(structuredFormat?.toLowerCase() as "json" | "yaml") || "json"}
|
||||
minHeight="300px"
|
||||
className="border-0"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
id="proposedContent"
|
||||
value={proposedContent}
|
||||
onChange={(e) => setProposedContent(e.target.value)}
|
||||
placeholder={t("proposedContentPlaceholder")}
|
||||
className="min-h-[300px] font-mono text-sm border-0 rounded-none focus-visible:ring-0"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isSkill ? (
|
||||
<SkillEditor
|
||||
value={proposedContent}
|
||||
onChange={setProposedContent}
|
||||
/>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<VariableToolbar onInsert={handleInsertVariable} />
|
||||
{isStructured ? (
|
||||
<CodeEditor
|
||||
value={proposedContent}
|
||||
onChange={setProposedContent}
|
||||
language={(structuredFormat?.toLowerCase() as "json" | "yaml") || "json"}
|
||||
minHeight="300px"
|
||||
className="border-0"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
id="proposedContent"
|
||||
value={proposedContent}
|
||||
onChange={(e) => setProposedContent(e.target.value)}
|
||||
placeholder={t("proposedContentPlaceholder")}
|
||||
className="min-h-[300px] font-mono text-sm border-0 rounded-none focus-visible:ring-0"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="diff" className="mt-0">
|
||||
{hasContentChanges ? (
|
||||
{isSkill ? (
|
||||
<SkillDiffViewer original={currentContent} modified={proposedContent} />
|
||||
) : hasContentChanges ? (
|
||||
<DiffView
|
||||
original={currentContent}
|
||||
modified={proposedContent}
|
||||
|
||||
@@ -73,12 +73,36 @@ export function DownloadPromptDropdown({ promptId, promptSlug, promptType }: Dow
|
||||
}
|
||||
};
|
||||
|
||||
// For SKILL type, show a simple button instead of dropdown
|
||||
const handleDownloadSkill = async () => {
|
||||
const base = promptSlug ? `${promptId}_${promptSlug}` : promptId;
|
||||
const url = `/api/prompts/${base}/skill`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Failed to fetch");
|
||||
const blob = await response.blob();
|
||||
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
// Use slug for filename
|
||||
a.download = `${promptSlug || promptId}.skill`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
toast.success(t("downloadStarted"));
|
||||
} catch {
|
||||
toast.error(t("downloadFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
// For SKILL type, show a simple button that downloads .skill zip
|
||||
if (isSkill) {
|
||||
return (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDownload("md")}>
|
||||
<Button variant="ghost" size="sm" onClick={handleDownloadSkill}>
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
{t("downloadSkillMd")}
|
||||
{t("downloadSkill")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, Play, BadgeCheck, Volume2 } from "lucide-react";
|
||||
import { ArrowBigUp, Lock, Copy, ImageIcon, Download, Play, BadgeCheck, Volume2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CodeView } from "@/components/ui/code-view";
|
||||
import { toast } from "sonner";
|
||||
@@ -135,6 +135,30 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDownloadSkill = async () => {
|
||||
// Download .skill zip for skills
|
||||
const base = prompt.slug ? `${prompt.id}_${prompt.slug}` : prompt.id;
|
||||
const url = `/api/prompts/${base}/skill`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Failed to fetch");
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
// Use slug for filename
|
||||
const slug = prompt.slug || prompt.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
a.download = `${slug}.skill`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
toast.success(t("downloadStarted"));
|
||||
} catch {
|
||||
toast.error(t("downloadFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors flex flex-col ${hasMediaBackground || isAudio ? "" : "p-4"}`}
|
||||
@@ -319,7 +343,15 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</button>
|
||||
{contentHasVariables ? (
|
||||
{prompt.type === "SKILL" ? (
|
||||
<button
|
||||
onClick={handleDownloadSkill}
|
||||
className="h-6 w-6 rounded hover:bg-accent flex items-center justify-center"
|
||||
title={t("download")}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : contentHasVariables ? (
|
||||
<button
|
||||
onClick={handleRunClick}
|
||||
className="h-6 w-6 rounded hover:bg-accent flex items-center justify-center"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { StructuredFormatWarning } from "./structured-format-warning";
|
||||
import { ContributorSearch } from "./contributor-search";
|
||||
import { PromptBuilder, type PromptBuilderHandle } from "./prompt-builder";
|
||||
import { MediaGenerator } from "./media-generator";
|
||||
import { SkillEditor } from "./skill-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -1143,18 +1144,10 @@ export function PromptForm({ categories, tags, initialData, initialContributors
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
{promptType === "SKILL" ? (
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<VariableToolbar onInsert={insertVariable} getSelectedText={getSelectedText} />
|
||||
<CodeEditor
|
||||
ref={codeEditorRef}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
language="markdown"
|
||||
placeholder={`---\nname: my-skill-name\ndescription: A clear description of what this skill does and when to use it\n---\n\n# My Skill\n\nDescribe what this skill does and how the agent should use it.\n\n## Instructions\n\n- Step 1: ...\n- Step 2: ...`}
|
||||
minHeight="400px"
|
||||
className="border-0 rounded-none"
|
||||
/>
|
||||
</div>
|
||||
<SkillEditor
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
) : isStructuredInput ? (
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<VariableToolbar onInsert={insertVariable} getSelectedText={getSelectedText} />
|
||||
@@ -1202,15 +1195,17 @@ export function PromptForm({ categories, tags, initialData, initialContributors
|
||||
onConvert={(converted) => form.setValue("content", converted)}
|
||||
/>
|
||||
|
||||
{/* Structured format detection warning */}
|
||||
<StructuredFormatWarning
|
||||
content={promptContent}
|
||||
isStructuredInput={isStructuredInput}
|
||||
onSwitchToStructured={(format) => {
|
||||
form.setValue("structuredFormat", format);
|
||||
form.setValue("type", "TEXT");
|
||||
}}
|
||||
/>
|
||||
{/* Structured format detection warning - hide for SKILL type */}
|
||||
{promptType !== "SKILL" && (
|
||||
<StructuredFormatWarning
|
||||
content={promptContent}
|
||||
isStructuredInput={isStructuredInput}
|
||||
onSwitchToStructured={(format) => {
|
||||
form.setValue("structuredFormat", format);
|
||||
form.setValue("type", "TEXT");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ===== LLM PROCESSING ARROW ===== */}
|
||||
|
||||
376
src/components/prompts/skill-diff-viewer.tsx
Normal file
376
src/components/prompts/skill-diff-viewer.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import {
|
||||
File,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Minus,
|
||||
Edit2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
parseSkillFiles,
|
||||
getLanguageFromFilename,
|
||||
DEFAULT_SKILL_FILE,
|
||||
type SkillFile,
|
||||
} from "@/lib/skill-files";
|
||||
|
||||
interface SkillDiffViewerProps {
|
||||
original: string;
|
||||
modified: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Tree node type for folder structure
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isFolder: boolean;
|
||||
children: TreeNode[];
|
||||
status?: "added" | "removed" | "modified" | "unchanged";
|
||||
}
|
||||
|
||||
// Build a tree structure from flat file paths with diff status
|
||||
function buildDiffFileTree(
|
||||
originalFiles: SkillFile[],
|
||||
modifiedFiles: SkillFile[]
|
||||
): TreeNode[] {
|
||||
const originalMap = new Map(originalFiles.map((f) => [f.filename, f.content]));
|
||||
const modifiedMap = new Map(modifiedFiles.map((f) => [f.filename, f.content]));
|
||||
|
||||
// Get all unique filenames
|
||||
const allFilenames = new Set([
|
||||
...originalFiles.map((f) => f.filename),
|
||||
...modifiedFiles.map((f) => f.filename),
|
||||
]);
|
||||
|
||||
const root: TreeNode[] = [];
|
||||
|
||||
for (const filename of allFilenames) {
|
||||
const parts = filename.split("/");
|
||||
let currentLevel = root;
|
||||
|
||||
// Determine file status
|
||||
let status: TreeNode["status"] = "unchanged";
|
||||
const origContent = originalMap.get(filename);
|
||||
const modContent = modifiedMap.get(filename);
|
||||
|
||||
if (origContent === undefined) {
|
||||
status = "added";
|
||||
} else if (modContent === undefined) {
|
||||
status = "removed";
|
||||
} else if (origContent !== modContent) {
|
||||
status = "modified";
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isLastPart = i === parts.length - 1;
|
||||
const currentPath = parts.slice(0, i + 1).join("/");
|
||||
|
||||
let existing = currentLevel.find((n) => n.name === part);
|
||||
|
||||
if (!existing) {
|
||||
existing = {
|
||||
name: part,
|
||||
path: currentPath,
|
||||
isFolder: !isLastPart,
|
||||
children: [],
|
||||
status: isLastPart ? status : undefined,
|
||||
};
|
||||
currentLevel.push(existing);
|
||||
} else if (isLastPart) {
|
||||
existing.status = status;
|
||||
}
|
||||
|
||||
if (!isLastPart) {
|
||||
currentLevel = existing.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: folders first, then alphabetically
|
||||
const sortNodes = (nodes: TreeNode[]): TreeNode[] => {
|
||||
return nodes
|
||||
.map((n) => ({ ...n, children: sortNodes(n.children) }))
|
||||
.sort((a, b) => {
|
||||
if (a.isFolder && !b.isFolder) return -1;
|
||||
if (!a.isFolder && b.isFolder) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
return sortNodes(root);
|
||||
}
|
||||
|
||||
// Recursive tree node component
|
||||
interface TreeNodeItemProps {
|
||||
node: TreeNode;
|
||||
depth: number;
|
||||
activeFile: string;
|
||||
expandedFolders: Set<string>;
|
||||
onToggleFolder: (path: string) => void;
|
||||
onOpenFile: (path: string) => void;
|
||||
}
|
||||
|
||||
function TreeNodeItem({
|
||||
node,
|
||||
depth,
|
||||
activeFile,
|
||||
expandedFolders,
|
||||
onToggleFolder,
|
||||
onOpenFile,
|
||||
}: TreeNodeItemProps) {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isActive = activeFile === node.path;
|
||||
const paddingLeft = depth * 12;
|
||||
|
||||
const statusColors = {
|
||||
added: "text-green-600 dark:text-green-400",
|
||||
removed: "text-red-600 dark:text-red-400",
|
||||
modified: "text-amber-600 dark:text-amber-400",
|
||||
unchanged: "text-muted-foreground",
|
||||
};
|
||||
|
||||
const StatusIcon = node.status === "added"
|
||||
? Plus
|
||||
: node.status === "removed"
|
||||
? Minus
|
||||
: node.status === "modified"
|
||||
? Edit2
|
||||
: null;
|
||||
|
||||
if (node.isFolder) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors hover:bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onToggleFolder(node.path)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div>
|
||||
{node.children.map((child) => (
|
||||
<TreeNodeItem
|
||||
key={child.path}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={onToggleFolder}
|
||||
onOpenFile={onOpenFile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// File node
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors",
|
||||
isActive ? "bg-primary/10 text-primary" : "hover:bg-muted",
|
||||
node.status && statusColors[node.status]
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onOpenFile(node.path)}
|
||||
>
|
||||
<span className="w-3 shrink-0" />
|
||||
<File className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
{StatusIcon && (
|
||||
<StatusIcon className="h-3 w-3 mr-1 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillDiffViewer({ original, modified, className }: SkillDiffViewerProps) {
|
||||
const t = useTranslations("prompts");
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
// Parse files from both versions
|
||||
const originalFiles = useMemo(() => parseSkillFiles(original), [original]);
|
||||
const modifiedFiles = useMemo(() => parseSkillFiles(modified), [modified]);
|
||||
|
||||
const [activeFile, setActiveFile] = useState<string>(DEFAULT_SKILL_FILE);
|
||||
const [editorKey, setEditorKey] = useState(0);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
|
||||
// Handle file change with editor remount
|
||||
const handleFileChange = useCallback((filename: string) => {
|
||||
if (filename !== activeFile) {
|
||||
setEditorKey((k) => k + 1); // Force new editor instance
|
||||
setActiveFile(filename);
|
||||
}
|
||||
}, [activeFile]);
|
||||
|
||||
// Build tree structure with diff status
|
||||
const fileTree = useMemo(
|
||||
() => buildDiffFileTree(originalFiles, modifiedFiles),
|
||||
[originalFiles, modifiedFiles]
|
||||
);
|
||||
|
||||
// Get original and modified content for active file
|
||||
const originalContent = useMemo(
|
||||
() => originalFiles.find((f) => f.filename === activeFile)?.content || "",
|
||||
[originalFiles, activeFile]
|
||||
);
|
||||
const modifiedContent = useMemo(
|
||||
() => modifiedFiles.find((f) => f.filename === activeFile)?.content || "",
|
||||
[modifiedFiles, activeFile]
|
||||
);
|
||||
|
||||
const activeLanguage = useMemo(
|
||||
() => getLanguageFromFilename(activeFile),
|
||||
[activeFile]
|
||||
);
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = useCallback((folderPath: string) => {
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(folderPath)) {
|
||||
next.delete(folderPath);
|
||||
} else {
|
||||
next.add(folderPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Count changes
|
||||
const changeCount = useMemo(() => {
|
||||
let added = 0, removed = 0, modified = 0;
|
||||
const origMap = new Map(originalFiles.map((f) => [f.filename, f.content]));
|
||||
const modMap = new Map(modifiedFiles.map((f) => [f.filename, f.content]));
|
||||
|
||||
for (const f of modifiedFiles) {
|
||||
if (!origMap.has(f.filename)) added++;
|
||||
else if (origMap.get(f.filename) !== f.content) modified++;
|
||||
}
|
||||
for (const f of originalFiles) {
|
||||
if (!modMap.has(f.filename)) removed++;
|
||||
}
|
||||
return { added, removed, modified };
|
||||
}, [originalFiles, modifiedFiles]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex border rounded-lg overflow-hidden bg-background",
|
||||
className
|
||||
)}
|
||||
style={{ height: "500px" }}
|
||||
>
|
||||
{/* Sidebar - File Tree */}
|
||||
<div className="w-56 border-r bg-muted/30 flex flex-col shrink-0">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b bg-muted/50">
|
||||
<FolderOpen className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">{t("skillFiles")}</span>
|
||||
</div>
|
||||
|
||||
{/* Change Summary */}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 border-b bg-muted/30 text-xs">
|
||||
{changeCount.added > 0 && (
|
||||
<Badge variant="outline" className="text-green-600 border-green-600/30 bg-green-500/10 text-[10px] px-1.5 py-0">
|
||||
+{changeCount.added}
|
||||
</Badge>
|
||||
)}
|
||||
{changeCount.removed > 0 && (
|
||||
<Badge variant="outline" className="text-red-600 border-red-600/30 bg-red-500/10 text-[10px] px-1.5 py-0">
|
||||
-{changeCount.removed}
|
||||
</Badge>
|
||||
)}
|
||||
{changeCount.modified > 0 && (
|
||||
<Badge variant="outline" className="text-amber-600 border-amber-600/30 bg-amber-500/10 text-[10px] px-1.5 py-0">
|
||||
~{changeCount.modified}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File Tree */}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{fileTree.map((node) => (
|
||||
<TreeNodeItem
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={toggleFolder}
|
||||
onOpenFile={handleFileChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Diff Editor Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Tab/File Header */}
|
||||
<div className="flex items-center justify-between border-b bg-muted/30 px-3 py-1.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<File className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs font-mono truncate">{activeFile}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monaco Diff Editor */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<DiffEditor
|
||||
key={`${editorKey}-${activeFile}`}
|
||||
height="100%"
|
||||
language={activeLanguage}
|
||||
original={originalContent}
|
||||
modified={modifiedContent}
|
||||
theme={resolvedTheme === "dark" ? "vs-dark" : "light"}
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 12,
|
||||
lineNumbers: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
renderSideBySide: true,
|
||||
originalEditable: false,
|
||||
renderOverviewRuler: false,
|
||||
scrollbar: {
|
||||
vertical: "auto",
|
||||
horizontal: "auto",
|
||||
verticalScrollbarSize: 8,
|
||||
horizontalScrollbarSize: 8,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
556
src/components/prompts/skill-editor.tsx
Normal file
556
src/components/prompts/skill-editor.tsx
Normal file
@@ -0,0 +1,556 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import Editor, { type OnMount } from "@monaco-editor/react";
|
||||
import {
|
||||
File,
|
||||
FilePlus,
|
||||
Trash2,
|
||||
X,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
parseSkillFiles,
|
||||
serializeSkillFiles,
|
||||
getLanguageFromFilename,
|
||||
validateFilename,
|
||||
suggestFilename,
|
||||
DEFAULT_SKILL_FILE,
|
||||
DEFAULT_SKILL_CONTENT,
|
||||
type SkillFile,
|
||||
} from "@/lib/skill-files";
|
||||
|
||||
interface SkillEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Tree node type for folder structure
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isFolder: boolean;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
// Build a tree structure from flat file paths
|
||||
function buildFileTree(files: SkillFile[]): TreeNode[] {
|
||||
const root: TreeNode[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const parts = file.filename.split("/");
|
||||
let currentLevel = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isLastPart = i === parts.length - 1;
|
||||
const currentPath = parts.slice(0, i + 1).join("/");
|
||||
|
||||
let existing = currentLevel.find((n) => n.name === part);
|
||||
|
||||
if (!existing) {
|
||||
existing = {
|
||||
name: part,
|
||||
path: currentPath,
|
||||
isFolder: !isLastPart,
|
||||
children: [],
|
||||
};
|
||||
currentLevel.push(existing);
|
||||
}
|
||||
|
||||
if (!isLastPart) {
|
||||
currentLevel = existing.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: folders first, then alphabetically
|
||||
const sortNodes = (nodes: TreeNode[]): TreeNode[] => {
|
||||
return nodes
|
||||
.map((n) => ({ ...n, children: sortNodes(n.children) }))
|
||||
.sort((a, b) => {
|
||||
if (a.isFolder && !b.isFolder) return -1;
|
||||
if (!a.isFolder && b.isFolder) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
return sortNodes(root);
|
||||
}
|
||||
|
||||
// Recursive tree node component
|
||||
interface TreeNodeItemProps {
|
||||
node: TreeNode;
|
||||
depth: number;
|
||||
activeFile: string;
|
||||
expandedFolders: Set<string>;
|
||||
onToggleFolder: (path: string) => void;
|
||||
onOpenFile: (path: string) => void;
|
||||
onDeleteFile: (path: string) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
function TreeNodeItem({
|
||||
node,
|
||||
depth,
|
||||
activeFile,
|
||||
expandedFolders,
|
||||
onToggleFolder,
|
||||
onOpenFile,
|
||||
onDeleteFile,
|
||||
t,
|
||||
}: TreeNodeItemProps) {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isActive = activeFile === node.path;
|
||||
const paddingLeft = depth * 12;
|
||||
|
||||
if (node.isFolder) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors hover:bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onToggleFolder(node.path)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div>
|
||||
{node.children.map((child) => (
|
||||
<TreeNodeItem
|
||||
key={child.path}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={onToggleFolder}
|
||||
onOpenFile={onOpenFile}
|
||||
onDeleteFile={onDeleteFile}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// File node
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors",
|
||||
isActive ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onOpenFile(node.path)}
|
||||
>
|
||||
<span className="w-3 shrink-0" /> {/* Spacer for alignment */}
|
||||
<File className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
{node.path !== DEFAULT_SKILL_FILE && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteFile(node.path);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-destructive/10 rounded transition-opacity mr-1"
|
||||
title={t("deleteFile")}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillEditor({ value, onChange, className }: SkillEditorProps) {
|
||||
const t = useTranslations("prompts");
|
||||
const tCommon = useTranslations("common");
|
||||
const { resolvedTheme } = useTheme();
|
||||
const editorRef = useRef<Parameters<OnMount>[0] | null>(null);
|
||||
|
||||
// Parse files from the serialized content
|
||||
const [files, setFiles] = useState<SkillFile[]>(() => parseSkillFiles(value));
|
||||
const [activeFile, setActiveFile] = useState<string>(DEFAULT_SKILL_FILE);
|
||||
const [openTabs, setOpenTabs] = useState<string[]>([DEFAULT_SKILL_FILE]);
|
||||
|
||||
// Dialog states
|
||||
const [showNewFileDialog, setShowNewFileDialog] = useState(false);
|
||||
const [newFilename, setNewFilename] = useState("");
|
||||
const [filenameError, setFilenameError] = useState<string | null>(null);
|
||||
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
|
||||
|
||||
// Expanded folders state
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
|
||||
// Build tree structure from files
|
||||
const fileTree = useMemo(() => buildFileTree(files), [files]);
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = useCallback((folderPath: string) => {
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(folderPath)) {
|
||||
next.delete(folderPath);
|
||||
} else {
|
||||
next.add(folderPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Get the active file's content and language
|
||||
const activeFileData = useMemo(
|
||||
() => files.find((f) => f.filename === activeFile),
|
||||
[files, activeFile]
|
||||
);
|
||||
const activeLanguage = useMemo(
|
||||
() => getLanguageFromFilename(activeFile),
|
||||
[activeFile]
|
||||
);
|
||||
|
||||
// Debounced onChange to parent
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const debouncedOnChange = useCallback(
|
||||
(newFiles: SkillFile[]) => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
onChange(serializeSkillFiles(newFiles));
|
||||
}, 300);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
// Cleanup debounce timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Serialize and propagate changes to parent (debounced)
|
||||
const updateFiles = useCallback(
|
||||
(newFiles: SkillFile[]) => {
|
||||
setFiles(newFiles);
|
||||
debouncedOnChange(newFiles);
|
||||
},
|
||||
[debouncedOnChange]
|
||||
);
|
||||
|
||||
// Handle editor content changes
|
||||
const handleEditorChange = useCallback(
|
||||
(newContent: string | undefined) => {
|
||||
const content = newContent || "";
|
||||
const newFiles = files.map((f) =>
|
||||
f.filename === activeFile ? { ...f, content } : f
|
||||
);
|
||||
updateFiles(newFiles);
|
||||
},
|
||||
[files, activeFile, updateFiles]
|
||||
);
|
||||
|
||||
// Open a file in a tab
|
||||
const openFile = useCallback((filename: string) => {
|
||||
setActiveFile(filename);
|
||||
setOpenTabs((prev) =>
|
||||
prev.includes(filename) ? prev : [...prev, filename]
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Close a tab
|
||||
const closeTab = useCallback(
|
||||
(filename: string, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
if (filename === DEFAULT_SKILL_FILE) return; // Can't close SKILL.md
|
||||
|
||||
setOpenTabs((prev) => {
|
||||
const newTabs = prev.filter((f) => f !== filename);
|
||||
if (activeFile === filename) {
|
||||
setActiveFile(newTabs[newTabs.length - 1] || DEFAULT_SKILL_FILE);
|
||||
}
|
||||
return newTabs;
|
||||
});
|
||||
},
|
||||
[activeFile]
|
||||
);
|
||||
|
||||
// Add a new file
|
||||
const handleAddFile = useCallback(() => {
|
||||
const suggestion = suggestFilename(files.map((f) => f.filename));
|
||||
setNewFilename(suggestion);
|
||||
setFilenameError(null);
|
||||
setShowNewFileDialog(true);
|
||||
}, [files]);
|
||||
|
||||
const confirmAddFile = useCallback(() => {
|
||||
const errorCode = validateFilename(
|
||||
newFilename,
|
||||
files.map((f) => f.filename)
|
||||
);
|
||||
if (errorCode) {
|
||||
// Translate error code
|
||||
setFilenameError(t(`validation.${errorCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = newFilename.trim();
|
||||
const newFiles = [...files, { filename: trimmed, content: "" }];
|
||||
updateFiles(newFiles);
|
||||
openFile(trimmed);
|
||||
setShowNewFileDialog(false);
|
||||
setNewFilename("");
|
||||
}, [newFilename, files, updateFiles, openFile, t]);
|
||||
|
||||
// Delete a file
|
||||
const handleDeleteFile = useCallback((filename: string) => {
|
||||
if (filename === DEFAULT_SKILL_FILE) return;
|
||||
setFileToDelete(filename);
|
||||
}, []);
|
||||
|
||||
const confirmDeleteFile = useCallback(() => {
|
||||
if (!fileToDelete || fileToDelete === DEFAULT_SKILL_FILE) return;
|
||||
|
||||
const newFiles = files.filter((f) => f.filename !== fileToDelete);
|
||||
updateFiles(newFiles);
|
||||
closeTab(fileToDelete);
|
||||
setFileToDelete(null);
|
||||
}, [fileToDelete, files, updateFiles, closeTab]);
|
||||
|
||||
// Re-parse when external value changes significantly
|
||||
useEffect(() => {
|
||||
const parsed = parseSkillFiles(value);
|
||||
const currentSerialized = serializeSkillFiles(files);
|
||||
|
||||
// Only update if the value changed externally
|
||||
if (value !== currentSerialized) {
|
||||
setFiles(parsed);
|
||||
// Ensure active file exists
|
||||
if (!parsed.some((f) => f.filename === activeFile)) {
|
||||
setActiveFile(DEFAULT_SKILL_FILE);
|
||||
setOpenTabs([DEFAULT_SKILL_FILE]);
|
||||
}
|
||||
}
|
||||
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleEditorMount: OnMount = useCallback((editor) => {
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
// File icon based on extension
|
||||
const getFileIcon = (filename: string) => {
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
// Could add more specific icons here
|
||||
return <File className="h-4 w-4 text-muted-foreground" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex border rounded-lg overflow-hidden bg-background",
|
||||
className
|
||||
)}
|
||||
style={{ height: "500px" }}
|
||||
>
|
||||
{/* Sidebar - File Tree */}
|
||||
<div className="w-56 border-r bg-muted/30 flex flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/50">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<FolderOpen className="h-4 w-4 text-primary" />
|
||||
<span>{t("skillFiles")}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handleAddFile}
|
||||
title={t("addFile")}
|
||||
>
|
||||
<FilePlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* File Tree */}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{fileTree.map((node) => (
|
||||
<TreeNodeItem
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={toggleFolder}
|
||||
onOpenFile={openFile}
|
||||
onDeleteFile={handleDeleteFile}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sidebar Footer - File Count */}
|
||||
<div className="px-3 py-2 border-t bg-muted/50 text-xs text-muted-foreground">
|
||||
{files.length} {files.length === 1 ? t("file") : t("files")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Editor Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center border-b bg-muted/30 overflow-x-auto">
|
||||
{openTabs.map((filename) => (
|
||||
<div
|
||||
key={filename}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 border-r cursor-pointer text-xs font-mono transition-colors whitespace-nowrap",
|
||||
activeFile === filename
|
||||
? "bg-background border-b-2 border-b-primary -mb-px"
|
||||
: "bg-muted/50 hover:bg-muted"
|
||||
)}
|
||||
onClick={() => setActiveFile(filename)}
|
||||
>
|
||||
{getFileIcon(filename)}
|
||||
<span className="max-w-[120px] truncate">{filename}</span>
|
||||
{filename !== DEFAULT_SKILL_FILE && (
|
||||
<button
|
||||
onClick={(e) => closeTab(filename, e)}
|
||||
className="ml-1 p-0.5 hover:bg-muted rounded"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Monaco Editor */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<Editor
|
||||
height="100%"
|
||||
language={activeLanguage}
|
||||
value={activeFileData?.content || ""}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorMount}
|
||||
theme={resolvedTheme === "dark" ? "vs-dark" : "light"}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 12,
|
||||
lineNumbers: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
wrappingIndent: "indent",
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
padding: { top: 8, bottom: 8 },
|
||||
renderLineHighlight: "line",
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
scrollbar: {
|
||||
vertical: "auto",
|
||||
horizontal: "auto",
|
||||
verticalScrollbarSize: 8,
|
||||
horizontalScrollbarSize: 8,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New File Dialog */}
|
||||
<Dialog open={showNewFileDialog} onOpenChange={setShowNewFileDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("addNewFile")}</DialogTitle>
|
||||
<DialogDescription>{t("addNewFileDescription")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input
|
||||
value={newFilename}
|
||||
onChange={(e) => {
|
||||
setNewFilename(e.target.value);
|
||||
setFilenameError(null);
|
||||
}}
|
||||
placeholder="filename.ext"
|
||||
className="font-mono"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
confirmAddFile();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
{filenameError && (
|
||||
<p className="text-sm text-destructive mt-2">{filenameError}</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowNewFileDialog(false)}
|
||||
>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button onClick={confirmAddFile}>{t("addFile")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={!!fileToDelete}
|
||||
onOpenChange={(open) => !open && setFileToDelete(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("deleteFileConfirm")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("deleteFileDescription", { filename: fileToDelete || "" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setFileToDelete(null)}>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={confirmDeleteFile}>
|
||||
{tCommon("delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
373
src/components/prompts/skill-viewer.tsx
Normal file
373
src/components/prompts/skill-viewer.tsx
Normal file
@@ -0,0 +1,373 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import {
|
||||
File,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Copy,
|
||||
Check,
|
||||
Download,
|
||||
Package,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
parseSkillFiles,
|
||||
getLanguageFromFilename,
|
||||
DEFAULT_SKILL_FILE,
|
||||
type SkillFile,
|
||||
} from "@/lib/skill-files";
|
||||
|
||||
interface SkillViewerProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
promptId?: string;
|
||||
promptSlug?: string;
|
||||
}
|
||||
|
||||
// Tree node type for folder structure
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isFolder: boolean;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
// Build a tree structure from flat file paths
|
||||
function buildFileTree(files: SkillFile[]): TreeNode[] {
|
||||
const root: TreeNode[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const parts = file.filename.split("/");
|
||||
let currentLevel = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isLastPart = i === parts.length - 1;
|
||||
const currentPath = parts.slice(0, i + 1).join("/");
|
||||
|
||||
let existing = currentLevel.find((n) => n.name === part);
|
||||
|
||||
if (!existing) {
|
||||
existing = {
|
||||
name: part,
|
||||
path: currentPath,
|
||||
isFolder: !isLastPart,
|
||||
children: [],
|
||||
};
|
||||
currentLevel.push(existing);
|
||||
}
|
||||
|
||||
if (!isLastPart) {
|
||||
currentLevel = existing.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: folders first, then alphabetically
|
||||
const sortNodes = (nodes: TreeNode[]): TreeNode[] => {
|
||||
return nodes
|
||||
.map((n) => ({ ...n, children: sortNodes(n.children) }))
|
||||
.sort((a, b) => {
|
||||
if (a.isFolder && !b.isFolder) return -1;
|
||||
if (!a.isFolder && b.isFolder) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
return sortNodes(root);
|
||||
}
|
||||
|
||||
// Recursive tree node component
|
||||
interface TreeNodeItemProps {
|
||||
node: TreeNode;
|
||||
depth: number;
|
||||
activeFile: string;
|
||||
expandedFolders: Set<string>;
|
||||
onToggleFolder: (path: string) => void;
|
||||
onOpenFile: (path: string) => void;
|
||||
}
|
||||
|
||||
function TreeNodeItem({
|
||||
node,
|
||||
depth,
|
||||
activeFile,
|
||||
expandedFolders,
|
||||
onToggleFolder,
|
||||
onOpenFile,
|
||||
}: TreeNodeItemProps) {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isActive = activeFile === node.path;
|
||||
const paddingLeft = depth * 12;
|
||||
|
||||
if (node.isFolder) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors hover:bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onToggleFolder(node.path)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div>
|
||||
{node.children.map((child) => (
|
||||
<TreeNodeItem
|
||||
key={child.path}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={onToggleFolder}
|
||||
onOpenFile={onOpenFile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// File node
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex items-center gap-1 py-1 rounded-md cursor-pointer text-sm transition-colors",
|
||||
isActive ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${paddingLeft + 4}px` }}
|
||||
onClick={() => onOpenFile(node.path)}
|
||||
>
|
||||
<span className="w-3 shrink-0" />
|
||||
<File className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="flex-1 truncate font-mono text-xs">{node.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillViewer({ content, className, promptId, promptSlug }: SkillViewerProps) {
|
||||
const t = useTranslations("prompts");
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
// Parse files from the serialized content
|
||||
const files = useMemo(() => parseSkillFiles(content), [content]);
|
||||
const [activeFile, setActiveFile] = useState<string>(DEFAULT_SKILL_FILE);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Build tree structure from files
|
||||
const fileTree = useMemo(() => buildFileTree(files), [files]);
|
||||
|
||||
// Get the active file's content and language
|
||||
const activeFileData = useMemo(
|
||||
() => files.find((f) => f.filename === activeFile),
|
||||
[files, activeFile]
|
||||
);
|
||||
const activeLanguage = useMemo(
|
||||
() => getLanguageFromFilename(activeFile),
|
||||
[activeFile]
|
||||
);
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = useCallback((folderPath: string) => {
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(folderPath)) {
|
||||
next.delete(folderPath);
|
||||
} else {
|
||||
next.add(folderPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Copy current file content
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (activeFileData) {
|
||||
await navigator.clipboard.writeText(activeFileData.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [activeFileData]);
|
||||
|
||||
// Download current file
|
||||
const handleDownload = useCallback(() => {
|
||||
if (activeFileData) {
|
||||
const blob = new Blob([activeFileData.content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = activeFile.split("/").pop() || activeFile;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}, [activeFileData, activeFile]);
|
||||
|
||||
// Download entire skill as .skill zip
|
||||
const handleDownloadSkill = useCallback(async () => {
|
||||
if (!promptId) return;
|
||||
const base = promptSlug ? `${promptId}_${promptSlug}` : promptId;
|
||||
const url = `/api/prompts/${base}/skill`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Failed to fetch");
|
||||
const blob = await response.blob();
|
||||
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
// Use slug for filename, fallback to promptId
|
||||
a.download = `${promptSlug || promptId}.skill`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
toast.success(t("downloadStarted"));
|
||||
} catch {
|
||||
toast.error(t("downloadFailed"));
|
||||
}
|
||||
}, [promptId, promptSlug, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex border rounded-lg overflow-hidden bg-background",
|
||||
className
|
||||
)}
|
||||
style={{ height: "500px" }}
|
||||
>
|
||||
{/* Sidebar - File Tree */}
|
||||
<div className="w-56 border-r bg-muted/30 flex flex-col shrink-0">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b bg-muted/50">
|
||||
<FolderOpen className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">{t("skillFiles")}</span>
|
||||
</div>
|
||||
|
||||
{/* File Tree */}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{fileTree.map((node) => (
|
||||
<TreeNodeItem
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeFile={activeFile}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={toggleFolder}
|
||||
onOpenFile={setActiveFile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sidebar Footer - File Count */}
|
||||
<div className="px-3 py-2 border-t bg-muted/50 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>{files.length} {files.length === 1 ? t("file") : t("files")}</span>
|
||||
{promptId && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs gap-1"
|
||||
onClick={handleDownloadSkill}
|
||||
>
|
||||
<Package className="h-3 w-3" />
|
||||
{t("downloadSkill")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Editor Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Tab/File Header */}
|
||||
<div className="flex items-center justify-between border-b bg-muted/30 px-3 py-1.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<File className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs font-mono truncate">{activeFile}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleCopy}
|
||||
title={t("copy")}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleDownload}
|
||||
title={t("download")}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monaco Editor (read-only) */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<Editor
|
||||
height="100%"
|
||||
language={activeLanguage}
|
||||
value={activeFileData?.content || ""}
|
||||
theme={resolvedTheme === "dark" ? "vs-dark" : "light"}
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 12,
|
||||
lineNumbers: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
wrappingIndent: "indent",
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
padding: { top: 8, bottom: 8 },
|
||||
renderLineHighlight: "none",
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
domReadOnly: true,
|
||||
scrollbar: {
|
||||
vertical: "auto",
|
||||
horizontal: "auto",
|
||||
verticalScrollbarSize: 8,
|
||||
horizontalScrollbarSize: 8,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
280
src/lib/skill-files.ts
Normal file
280
src/lib/skill-files.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Utilities for parsing and serializing multi-file skill content.
|
||||
* Files are stored in a single text field with a special separator format
|
||||
* using ASCII control characters:
|
||||
*
|
||||
* file 1 content
|
||||
* \x1FFILE:filename.ext\x1E
|
||||
* file 2 content
|
||||
* \x1FFILE:another-file.md\x1E
|
||||
* file 3 content
|
||||
*
|
||||
* \x1F (ASCII 31, Unit Separator) and \x1E (ASCII 30, Record Separator)
|
||||
* are control characters designed for data delimiting that cannot appear
|
||||
* in normal text content, making them injection-proof.
|
||||
*/
|
||||
|
||||
export interface SkillFile {
|
||||
filename: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// Separator uses ASCII control characters:
|
||||
// \x1F (Unit Separator, ASCII 31) marks start
|
||||
// \x1E (Record Separator, ASCII 30) marks end
|
||||
// These cannot appear in normal text content, making injection impossible
|
||||
const FILE_SEPARATOR_REGEX = /\x1FFILE:(.+?)\x1E/g;
|
||||
const FILE_SEPARATOR_TEMPLATE = (filename: string) => `\x1FFILE:${filename}\x1E`;
|
||||
|
||||
// Default file that cannot be deleted
|
||||
export const DEFAULT_SKILL_FILE = "SKILL.md";
|
||||
|
||||
// Default content for a new skill
|
||||
export const DEFAULT_SKILL_CONTENT = `---
|
||||
name: my-skill-name
|
||||
description: A clear description of what this skill does and when to use it
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Describe what this skill does and how the agent should use it.
|
||||
|
||||
## Instructions
|
||||
|
||||
- Step 1: ...
|
||||
- Step 2: ...
|
||||
`;
|
||||
|
||||
/**
|
||||
* Parse a serialized multi-file content string into an array of SkillFile objects.
|
||||
* The first chunk is always SKILL.md if no explicit filename is found.
|
||||
*/
|
||||
export function parseSkillFiles(content: string): SkillFile[] {
|
||||
if (!content || content.trim() === "") {
|
||||
return [{ filename: DEFAULT_SKILL_FILE, content: DEFAULT_SKILL_CONTENT }];
|
||||
}
|
||||
|
||||
const files: SkillFile[] = [];
|
||||
const parts = content.split(FILE_SEPARATOR_REGEX);
|
||||
|
||||
// First part is always content (before any separator)
|
||||
// Then alternating: filename, content, filename, content...
|
||||
|
||||
if (parts.length === 1) {
|
||||
// No separators found - single file (SKILL.md)
|
||||
return [{ filename: DEFAULT_SKILL_FILE, content: parts[0].trim() }];
|
||||
}
|
||||
|
||||
// First content chunk belongs to SKILL.md
|
||||
files.push({ filename: DEFAULT_SKILL_FILE, content: parts[0].trim() });
|
||||
|
||||
// Process remaining parts (filename, content pairs)
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const filename = parts[i];
|
||||
const fileContent = (parts[i + 1] || "").trim();
|
||||
|
||||
if (filename && filename !== DEFAULT_SKILL_FILE) {
|
||||
files.push({ filename, content: fileContent });
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an array of SkillFile objects into a single content string.
|
||||
* SKILL.md content comes first, followed by other files with separators.
|
||||
*/
|
||||
export function serializeSkillFiles(files: SkillFile[]): string {
|
||||
if (files.length === 0) {
|
||||
return DEFAULT_SKILL_CONTENT;
|
||||
}
|
||||
|
||||
// Find SKILL.md - it should always be first
|
||||
const skillFile = files.find(f => f.filename === DEFAULT_SKILL_FILE);
|
||||
const otherFiles = files.filter(f => f.filename !== DEFAULT_SKILL_FILE);
|
||||
|
||||
let result = skillFile?.content || DEFAULT_SKILL_CONTENT;
|
||||
|
||||
// Append other files with separators
|
||||
for (const file of otherFiles) {
|
||||
result += `\n${FILE_SEPARATOR_TEMPLATE(file.filename)}\n${file.content}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the language for Monaco editor based on file extension
|
||||
*/
|
||||
export function getLanguageFromFilename(filename: string): string {
|
||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
// Markdown
|
||||
md: "markdown",
|
||||
mdx: "markdown",
|
||||
// JavaScript/TypeScript
|
||||
js: "javascript",
|
||||
jsx: "javascript",
|
||||
ts: "typescript",
|
||||
tsx: "typescript",
|
||||
mjs: "javascript",
|
||||
cjs: "javascript",
|
||||
// Web
|
||||
html: "html",
|
||||
htm: "html",
|
||||
css: "css",
|
||||
scss: "scss",
|
||||
less: "less",
|
||||
// Data
|
||||
json: "json",
|
||||
yaml: "yaml",
|
||||
yml: "yaml",
|
||||
xml: "xml",
|
||||
toml: "toml",
|
||||
// Shell/Config
|
||||
sh: "shell",
|
||||
bash: "shell",
|
||||
zsh: "shell",
|
||||
fish: "shell",
|
||||
env: "shell",
|
||||
// Python
|
||||
py: "python",
|
||||
pyw: "python",
|
||||
// Ruby
|
||||
rb: "ruby",
|
||||
// Go
|
||||
go: "go",
|
||||
// Rust
|
||||
rs: "rust",
|
||||
// C/C++
|
||||
c: "c",
|
||||
h: "c",
|
||||
cpp: "cpp",
|
||||
hpp: "cpp",
|
||||
cc: "cpp",
|
||||
// Java/Kotlin
|
||||
java: "java",
|
||||
kt: "kotlin",
|
||||
kts: "kotlin",
|
||||
// C#
|
||||
cs: "csharp",
|
||||
// PHP
|
||||
php: "php",
|
||||
// Swift
|
||||
swift: "swift",
|
||||
// SQL
|
||||
sql: "sql",
|
||||
// GraphQL
|
||||
graphql: "graphql",
|
||||
gql: "graphql",
|
||||
// Docker
|
||||
dockerfile: "dockerfile",
|
||||
// Misc
|
||||
txt: "plaintext",
|
||||
log: "plaintext",
|
||||
gitignore: "plaintext",
|
||||
editorconfig: "ini",
|
||||
ini: "ini",
|
||||
cfg: "ini",
|
||||
conf: "ini",
|
||||
};
|
||||
|
||||
// Handle special filenames
|
||||
const lowerFilename = filename.toLowerCase();
|
||||
if (lowerFilename === "dockerfile" || lowerFilename.startsWith("dockerfile.")) {
|
||||
return "dockerfile";
|
||||
}
|
||||
if (lowerFilename === "makefile" || lowerFilename === "gnumakefile") {
|
||||
return "makefile";
|
||||
}
|
||||
|
||||
return languageMap[ext] || "plaintext";
|
||||
}
|
||||
|
||||
// Validation error codes for translation
|
||||
export type FilenameValidationError =
|
||||
| "filenameEmpty"
|
||||
| "filenameInvalidChars"
|
||||
| "pathStartEndSlash"
|
||||
| "pathConsecutiveSlashes"
|
||||
| "pathContainsDotDot"
|
||||
| "filenameReserved"
|
||||
| "filenameDuplicate"
|
||||
| "pathTooLong";
|
||||
|
||||
/**
|
||||
* Validate a filename/path for the skill file system.
|
||||
* Allows directory paths like `src/utils/helper.ts`
|
||||
* Returns an error code for translation, or null if valid.
|
||||
*/
|
||||
export function validateFilename(filename: string, existingFiles: string[]): FilenameValidationError | null {
|
||||
if (!filename || filename.trim() === "") {
|
||||
return "filenameEmpty";
|
||||
}
|
||||
|
||||
const trimmed = filename.trim();
|
||||
|
||||
// Check for invalid characters (allow forward slashes for directories)
|
||||
if (/[<>:"|?*\\]/.test(trimmed)) {
|
||||
return "filenameInvalidChars";
|
||||
}
|
||||
|
||||
// Check for problematic path patterns
|
||||
if (trimmed.startsWith("/") || trimmed.endsWith("/")) {
|
||||
return "pathStartEndSlash";
|
||||
}
|
||||
if (trimmed.includes("//")) {
|
||||
return "pathConsecutiveSlashes";
|
||||
}
|
||||
if (trimmed.includes("..")) {
|
||||
return "pathContainsDotDot";
|
||||
}
|
||||
|
||||
// Check for reserved name
|
||||
if (trimmed === DEFAULT_SKILL_FILE) {
|
||||
return "filenameReserved";
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (existingFiles.some(f => f.toLowerCase() === trimmed.toLowerCase())) {
|
||||
return "filenameDuplicate";
|
||||
}
|
||||
|
||||
// Check length
|
||||
if (trimmed.length > 200) {
|
||||
return "pathTooLong";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a suggested filename based on common patterns
|
||||
*/
|
||||
export function suggestFilename(existingFiles: string[]): string {
|
||||
const suggestions = [
|
||||
"README.md",
|
||||
"config.json",
|
||||
"schema.json",
|
||||
"template.md",
|
||||
"example.ts",
|
||||
"utils.ts",
|
||||
"types.ts",
|
||||
"constants.ts",
|
||||
];
|
||||
|
||||
for (const suggestion of suggestions) {
|
||||
if (!existingFiles.some(f => f.toLowerCase() === suggestion.toLowerCase())) {
|
||||
return suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique name
|
||||
let counter = 1;
|
||||
while (existingFiles.some(f => f.toLowerCase() === `file${counter}.md`)) {
|
||||
counter++;
|
||||
}
|
||||
return `file${counter}.md`;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { isValidApiKeyFormat } from "@/lib/api-key";
|
||||
import { improvePrompt } from "@/lib/ai/improve-prompt";
|
||||
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE, DEFAULT_SKILL_CONTENT } from "@/lib/skill-files";
|
||||
|
||||
interface AuthenticatedUser {
|
||||
id: string;
|
||||
@@ -712,6 +713,520 @@ function createServer(options: ServerOptions = {}) {
|
||||
}
|
||||
);
|
||||
|
||||
// Save skill tool - create a new skill with multiple files
|
||||
server.registerTool(
|
||||
"save_skill",
|
||||
{
|
||||
title: "Save Skill",
|
||||
description:
|
||||
"Save a new Agent Skill to your prompts.chat account. Skills are multi-file prompts that can include SKILL.md (required), reference docs, scripts, and configuration files. Requires API key authentication.",
|
||||
inputSchema: {
|
||||
title: z.string().min(1).max(200).describe("Title of the skill"),
|
||||
description: z.string().max(500).optional().describe("Description of what the skill does"),
|
||||
files: z.array(z.object({
|
||||
filename: z.string().describe("File path (e.g., 'SKILL.md', 'reference.md', 'scripts/helper.py')"),
|
||||
content: z.string().describe("File content"),
|
||||
})).min(1).describe("Array of files. Must include SKILL.md as the main skill file."),
|
||||
tags: z.array(z.string()).max(10).optional().describe("Optional array of tag names"),
|
||||
category: z.string().optional().describe("Optional category slug"),
|
||||
isPrivate: z.boolean().optional().describe("Whether the skill is private (default: uses your account setting)"),
|
||||
},
|
||||
},
|
||||
async ({ title, description, files, tags, category, isPrivate }) => {
|
||||
if (!authenticatedUser) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Authentication required. Please provide an API key." }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure SKILL.md exists
|
||||
const hasSkillMd = files.some(f => f.filename === DEFAULT_SKILL_FILE);
|
||||
if (!hasSkillMd) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "SKILL.md file is required" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Serialize files to multi-file format
|
||||
const content = serializeSkillFiles(files.map(f => ({ filename: f.filename, content: f.content })));
|
||||
|
||||
// Determine privacy setting
|
||||
const shouldBePrivate = isPrivate !== undefined ? isPrivate : !authenticatedUser.mcpPromptsPublicByDefault;
|
||||
|
||||
// Find or create tags
|
||||
const tagConnections: { tag: { connect: { id: string } } }[] = [];
|
||||
if (tags && tags.length > 0) {
|
||||
for (const tagName of tags) {
|
||||
const tagSlug = slugify(tagName);
|
||||
if (!tagSlug) continue;
|
||||
|
||||
let tag = await db.tag.findUnique({ where: { slug: tagSlug } });
|
||||
if (!tag) {
|
||||
tag = await db.tag.create({
|
||||
data: { name: tagName, slug: tagSlug },
|
||||
});
|
||||
}
|
||||
tagConnections.push({ tag: { connect: { id: tag.id } } });
|
||||
}
|
||||
}
|
||||
|
||||
// Find category if provided
|
||||
let categoryId: string | undefined;
|
||||
if (category) {
|
||||
const cat = await db.category.findUnique({ where: { slug: category } });
|
||||
if (cat) categoryId = cat.id;
|
||||
}
|
||||
|
||||
// Create the skill
|
||||
const skill = await db.prompt.create({
|
||||
data: {
|
||||
title,
|
||||
slug: slugify(title),
|
||||
content,
|
||||
description: description || null,
|
||||
isPrivate: shouldBePrivate,
|
||||
type: "SKILL",
|
||||
authorId: authenticatedUser.id,
|
||||
categoryId: categoryId || null,
|
||||
tags: { create: tagConnections },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
isPrivate: true,
|
||||
createdAt: true,
|
||||
tags: { select: { tag: { select: { name: true, slug: true } } } },
|
||||
category: { select: { name: true, slug: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
skill: {
|
||||
...skill,
|
||||
files: files.map(f => f.filename),
|
||||
tags: skill.tags.map((t) => t.tag.name),
|
||||
category: skill.category?.name || null,
|
||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("MCP save_skill error:", error);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to save skill" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Add file to skill tool
|
||||
server.registerTool(
|
||||
"add_file_to_skill",
|
||||
{
|
||||
title: "Add File to Skill",
|
||||
description:
|
||||
"Add a new file to an existing Agent Skill. Use this to add reference docs, scripts, or configuration files to a skill you own.",
|
||||
inputSchema: {
|
||||
skillId: z.string().describe("The ID of the skill to add the file to"),
|
||||
filename: z.string().describe("File path (e.g., 'reference.md', 'scripts/helper.py', 'config/settings.json')"),
|
||||
content: z.string().describe("File content"),
|
||||
},
|
||||
},
|
||||
async ({ skillId, filename, content }) => {
|
||||
if (!authenticatedUser) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Authentication required. Please provide an API key." }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the skill
|
||||
const skill = await db.prompt.findFirst({
|
||||
where: {
|
||||
id: skillId,
|
||||
type: "SKILL",
|
||||
authorId: authenticatedUser.id,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: { id: true, content: true, title: true, slug: true },
|
||||
});
|
||||
|
||||
if (!skill) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Skill not found or you don't have permission to edit it" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse existing files
|
||||
const files = parseSkillFiles(skill.content);
|
||||
|
||||
// Check if file already exists
|
||||
if (files.some(f => f.filename === filename)) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: `File '${filename}' already exists. Use a different filename or update the existing file.` }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Cannot add SKILL.md (it always exists)
|
||||
if (filename === DEFAULT_SKILL_FILE) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "SKILL.md already exists. Edit the skill directly to modify it." }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Add the new file
|
||||
files.push({ filename, content });
|
||||
|
||||
// Serialize and update
|
||||
const updatedContent = serializeSkillFiles(files);
|
||||
await db.prompt.update({
|
||||
where: { id: skillId },
|
||||
data: { content: updatedContent, updatedAt: new Date() },
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
message: `File '${filename}' added to skill`,
|
||||
skillId,
|
||||
files: files.map(f => f.filename),
|
||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("MCP add_file_to_skill error:", error);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to add file to skill" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Remove file from skill tool
|
||||
server.registerTool(
|
||||
"remove_file_from_skill",
|
||||
{
|
||||
title: "Remove File from Skill",
|
||||
description:
|
||||
"Remove a file from an existing Agent Skill. Cannot remove SKILL.md as it is required.",
|
||||
inputSchema: {
|
||||
skillId: z.string().describe("The ID of the skill to remove the file from"),
|
||||
filename: z.string().describe("File path to remove (e.g., 'reference.md', 'scripts/helper.py')"),
|
||||
},
|
||||
},
|
||||
async ({ skillId, filename }) => {
|
||||
if (!authenticatedUser) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Authentication required. Please provide an API key." }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Cannot remove SKILL.md
|
||||
if (filename === DEFAULT_SKILL_FILE) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Cannot remove SKILL.md - it is required for all skills" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the skill
|
||||
const skill = await db.prompt.findFirst({
|
||||
where: {
|
||||
id: skillId,
|
||||
type: "SKILL",
|
||||
authorId: authenticatedUser.id,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: { id: true, content: true, title: true, slug: true },
|
||||
});
|
||||
|
||||
if (!skill) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Skill not found or you don't have permission to edit it" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse existing files
|
||||
const files = parseSkillFiles(skill.content);
|
||||
|
||||
// Check if file exists
|
||||
if (!files.some(f => f.filename === filename)) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: `File '${filename}' not found in this skill` }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
const updatedFiles = files.filter(f => f.filename !== filename);
|
||||
|
||||
// Serialize and update
|
||||
const updatedContent = serializeSkillFiles(updatedFiles);
|
||||
await db.prompt.update({
|
||||
where: { id: skillId },
|
||||
data: { content: updatedContent, updatedAt: new Date() },
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
message: `File '${filename}' removed from skill`,
|
||||
skillId,
|
||||
files: updatedFiles.map(f => f.filename),
|
||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("MCP remove_file_from_skill error:", error);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to remove file from skill" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get skill tool - retrieve a skill with all its files
|
||||
server.registerTool(
|
||||
"get_skill",
|
||||
{
|
||||
title: "Get Skill",
|
||||
description:
|
||||
"Get an Agent Skill by ID, including all its files (SKILL.md, reference docs, scripts, etc.). Returns the skill metadata and file contents.",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The ID of the skill to retrieve"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
try {
|
||||
// Build visibility filter
|
||||
const visibilityFilter = authenticatedUser
|
||||
? {
|
||||
OR: [
|
||||
{ isPrivate: false },
|
||||
{ isPrivate: true, authorId: authenticatedUser.id },
|
||||
],
|
||||
}
|
||||
: { isPrivate: false };
|
||||
|
||||
const skill = await db.prompt.findFirst({
|
||||
where: {
|
||||
id,
|
||||
type: "SKILL",
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
...visibilityFilter,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
content: true,
|
||||
isPrivate: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
author: { select: { username: true, name: true } },
|
||||
category: { select: { name: true, slug: true } },
|
||||
tags: { select: { tag: { select: { name: true, slug: true } } } },
|
||||
_count: { select: { votes: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!skill) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Skill not found" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse files from content
|
||||
const files = parseSkillFiles(skill.content);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
id: skill.id,
|
||||
slug: getPromptName(skill),
|
||||
title: skill.title,
|
||||
description: skill.description,
|
||||
author: skill.author.name || skill.author.username,
|
||||
category: skill.category?.name || null,
|
||||
tags: skill.tags.map((t) => t.tag.name),
|
||||
votes: skill._count.votes,
|
||||
isPrivate: skill.isPrivate,
|
||||
createdAt: skill.createdAt.toISOString(),
|
||||
updatedAt: skill.updatedAt.toISOString(),
|
||||
files: files.map(f => ({
|
||||
filename: f.filename,
|
||||
content: f.content,
|
||||
})),
|
||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("MCP get_skill error:", error);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to get skill" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Search skills tool - search for agent skills
|
||||
server.registerTool(
|
||||
"search_skills",
|
||||
{
|
||||
title: "Search Skills",
|
||||
description:
|
||||
"Search for Agent Skills by keyword. Returns matching skills with title, description, author, and file list. Use this to discover reusable AI agent capabilities for coding, analysis, automation, and more.",
|
||||
inputSchema: {
|
||||
query: z.string().describe("Search query to find relevant skills"),
|
||||
limit: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.default(10)
|
||||
.describe("Maximum number of skills to return (default 10, max 50)"),
|
||||
category: z.string().optional().describe("Filter by category slug"),
|
||||
tag: z.string().optional().describe("Filter by tag slug"),
|
||||
},
|
||||
},
|
||||
async ({ query, limit = 10, category, tag }) => {
|
||||
try {
|
||||
const where: Record<string, unknown> = {
|
||||
type: "SKILL",
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
AND: [
|
||||
// Search filter
|
||||
{
|
||||
OR: [
|
||||
{ title: { contains: query, mode: "insensitive" } },
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
{ content: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
// Visibility filter: public OR user's own private skills
|
||||
authenticatedUser
|
||||
? {
|
||||
OR: [
|
||||
{ isPrivate: false },
|
||||
{ isPrivate: true, authorId: authenticatedUser.id },
|
||||
],
|
||||
}
|
||||
: { isPrivate: false },
|
||||
],
|
||||
};
|
||||
|
||||
if (category) where.category = { slug: category };
|
||||
if (tag) where.tags = { some: { tag: { slug: tag } } };
|
||||
|
||||
const skills = await db.prompt.findMany({
|
||||
where,
|
||||
take: Math.min(limit, 50),
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
author: { select: { username: true, name: true } },
|
||||
category: { select: { name: true, slug: true } },
|
||||
tags: { select: { tag: { select: { name: true, slug: true } } } },
|
||||
_count: { select: { votes: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const results = skills.map((s) => {
|
||||
const files = parseSkillFiles(s.content);
|
||||
return {
|
||||
id: s.id,
|
||||
slug: getPromptName(s),
|
||||
title: s.title,
|
||||
description: s.description,
|
||||
author: s.author.name || s.author.username,
|
||||
category: s.category?.name || null,
|
||||
tags: s.tags.map((t) => t.tag.name),
|
||||
votes: s._count.votes,
|
||||
files: files.map(f => f.filename),
|
||||
createdAt: s.createdAt.toISOString(),
|
||||
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({ query, count: results.length, skills: results }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("MCP search_skills error:", error);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to search skills" }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -758,6 +1273,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
name: "improve_prompt",
|
||||
description: "Transform a basic prompt into a well-structured, comprehensive prompt using AI.",
|
||||
},
|
||||
{
|
||||
name: "save_skill",
|
||||
description: "Save a new Agent Skill with multiple files (requires API key authentication).",
|
||||
},
|
||||
{
|
||||
name: "add_file_to_skill",
|
||||
description: "Add a file to an existing Agent Skill (requires API key authentication).",
|
||||
},
|
||||
{
|
||||
name: "remove_file_from_skill",
|
||||
description: "Remove a file from an Agent Skill (requires API key authentication).",
|
||||
},
|
||||
{
|
||||
name: "get_skill",
|
||||
description: "Get an Agent Skill by ID with all its files.",
|
||||
},
|
||||
{
|
||||
name: "search_skills",
|
||||
description: "Search for Agent Skills by keyword.",
|
||||
},
|
||||
],
|
||||
prompts: {
|
||||
description: "All public prompts are available as MCP prompts. Use prompts/list to browse and prompts/get to retrieve with variable substitution.",
|
||||
|
||||
Reference in New Issue
Block a user