feat(messages): add hero prompt input translations for multiple languages

This commit is contained in:
Fatih Kadir Akın
2025-12-14 17:25:26 +03:00
parent 174011b020
commit 765ded6a06
21 changed files with 1755 additions and 53 deletions

View File

@@ -650,6 +650,20 @@
"tokens": "رموز",
"noChanges": "لا توجد تغييرات"
},
"heroPromptInput": {
"placeholder": "صف البرومبت الذي تريد إنشاءه...",
"ariaLabel": "صف البرومبت الذي تريد إنشاءه",
"submit": "إنشاء برومبت",
"hint": "انقر للبدء في الإنشاء بالذكاء الاصطناعي",
"modelName": "وكيل البرومبت",
"examples": {
"codeReview": "أنشئ مساعد مراجعة كود يكتشف الأخطاء",
"emailWriter": "أنشئ كاتب بريد إلكتروني احترافي لأي مناسبة",
"studyPlanner": "صمم منشئ خطط دراسية مخصصة",
"recipeGenerator": "أنشئ منشئ وصفات بناءً على المكونات المتوفرة",
"interviewCoach": "أنشئ مدرب تحضير للمقابلات"
}
},
"notFound": {
"title": "الصفحة غير موجودة",
"description": "الصفحة التي تبحث عنها غير موجودة أو تم نقلها.",
@@ -676,7 +690,11 @@
"currentPrompt": "قيد الإنشاء:",
"stateTitle": "العنوان",
"stateContent": "المحتوى",
"stateTags": "وسوم"
"stateTags": "وسوم",
"editAction1": "املأ الحقول الناقصة، حدّث الوسوم.",
"editAction2": "حسّن المتغيرات",
"editAction3": "استخدم المتغيرات",
"editAction4": "حوّل إلى أمر JSON"
},
"report": {
"report": "إبلاغ",

View File

@@ -650,6 +650,20 @@
"tokens": "Token",
"noChanges": "Keine Änderungen"
},
"heroPromptInput": {
"placeholder": "Beschreiben Sie den Prompt, den Sie erstellen möchten...",
"ariaLabel": "Beschreiben Sie den Prompt, den Sie erstellen möchten",
"submit": "Prompt erstellen",
"hint": "Klicken Sie, um mit KI zu erstellen",
"modelName": "Prompt-Agent",
"examples": {
"codeReview": "Erstellen Sie einen Code-Review-Assistenten, der Bugs findet",
"emailWriter": "Bauen Sie einen professionellen E-Mail-Verfasser für jeden Anlass",
"studyPlanner": "Entwerfen Sie einen personalisierten Lernplan-Generator",
"recipeGenerator": "Erstellen Sie einen Rezeptgenerator basierend auf verfügbaren Zutaten",
"interviewCoach": "Erstellen Sie einen Vorstellungsgespräch-Coach"
}
},
"notFound": {
"title": "Seite nicht gefunden",
"description": "Die gesuchte Seite existiert nicht oder wurde verschoben.",
@@ -676,7 +690,11 @@
"currentPrompt": "Erstellen:",
"stateTitle": "Titel",
"stateContent": "Inhalt",
"stateTags": "Tags"
"stateTags": "Tags",
"editAction1": "Fehlende Felder ausfüllen, Tags aktualisieren.",
"editAction2": "Variablen verbessern",
"editAction3": "Variablen verwenden",
"editAction4": "In JSON-Prompt konvertieren"
},
"report": {
"report": "Melden",

View File

@@ -660,6 +660,20 @@
"categories": "Categories",
"createPrompt": "Create Prompt"
},
"heroPromptInput": {
"placeholder": "Describe the prompt you want to build...",
"ariaLabel": "Describe the prompt you want to build",
"submit": "Create prompt",
"hint": "Click to start building with AI",
"modelName": "Prompt Agent",
"examples": {
"codeReview": "Create a code review assistant that catches bugs",
"emailWriter": "Build a professional email writer for any occasion",
"studyPlanner": "Design a personalized study plan generator",
"recipeGenerator": "Make a recipe creator based on available ingredients",
"interviewCoach": "Create an interview preparation coach"
}
},
"promptBuilder": {
"title": "Prompt Building Agent",
"openBuilder": "Prompt Agent",
@@ -676,7 +690,11 @@
"currentPrompt": "Building:",
"stateTitle": "Title",
"stateContent": "Content",
"stateTags": "tags"
"stateTags": "tags",
"editAction1": "Fill missing fields, update tags.",
"editAction2": "Make variables better",
"editAction3": "Use variables",
"editAction4": "Convert to JSON prompt"
},
"report": {
"report": "Report",

View File

@@ -650,6 +650,20 @@
"tokens": "tokens",
"noChanges": "Sin cambios"
},
"heroPromptInput": {
"placeholder": "Describe el prompt que quieres crear...",
"ariaLabel": "Describe el prompt que quieres crear",
"submit": "Crear prompt",
"hint": "Haz clic para empezar a crear con IA",
"modelName": "Agente de Prompts",
"examples": {
"codeReview": "Crea un asistente de revisión de código que detecte errores",
"emailWriter": "Construye un escritor de emails profesionales para cualquier ocasión",
"studyPlanner": "Diseña un generador de planes de estudio personalizados",
"recipeGenerator": "Crea un generador de recetas basado en ingredientes disponibles",
"interviewCoach": "Crea un coach de preparación para entrevistas"
}
},
"notFound": {
"title": "Página No Encontrada",
"description": "La página que buscas no existe o ha sido movida.",
@@ -676,7 +690,11 @@
"currentPrompt": "Construyendo:",
"stateTitle": "Título",
"stateContent": "Contenido",
"stateTags": "etiquetas"
"stateTags": "etiquetas",
"editAction1": "Completar campos faltantes, actualizar etiquetas.",
"editAction2": "Mejorar variables",
"editAction3": "Usar variables",
"editAction4": "Convertir a prompt JSON"
},
"report": {
"report": "Reportar",

View File

@@ -650,6 +650,20 @@
"tokens": "tokens",
"noChanges": "Aucune modification"
},
"heroPromptInput": {
"placeholder": "Décrivez le prompt que vous voulez créer...",
"ariaLabel": "Décrivez le prompt que vous voulez créer",
"submit": "Créer un prompt",
"hint": "Cliquez pour commencer à créer avec l'IA",
"modelName": "Agent de Prompts",
"examples": {
"codeReview": "Créez un assistant de revue de code qui détecte les bugs",
"emailWriter": "Construisez un rédacteur d'emails professionnels pour toute occasion",
"studyPlanner": "Concevez un générateur de plans d'études personnalisés",
"recipeGenerator": "Créez un générateur de recettes basé sur les ingrédients disponibles",
"interviewCoach": "Créez un coach de préparation aux entretiens"
}
},
"notFound": {
"title": "Page Non Trouvée",
"description": "La page que vous recherchez n'existe pas ou a été déplacée.",
@@ -676,7 +690,11 @@
"currentPrompt": "En construction :",
"stateTitle": "Titre",
"stateContent": "Contenu",
"stateTags": "tags"
"stateTags": "tags",
"editAction1": "Remplir les champs manquants, mettre à jour les tags.",
"editAction2": "Améliorer les variables",
"editAction3": "Utiliser des variables",
"editAction4": "Convertir en prompt JSON"
},
"report": {
"report": "Signaler",

View File

@@ -650,6 +650,20 @@
"tokens": "token",
"noChanges": "Nessuna modifica"
},
"heroPromptInput": {
"placeholder": "Descrivi il prompt che vuoi creare...",
"ariaLabel": "Descrivi il prompt che vuoi creare",
"submit": "Crea prompt",
"hint": "Clicca per iniziare a creare con l'IA",
"modelName": "Agente Prompt",
"examples": {
"codeReview": "Crea un assistente di revisione codice che rileva bug",
"emailWriter": "Costruisci uno scrittore di email professionali per ogni occasione",
"studyPlanner": "Progetta un generatore di piani di studio personalizzati",
"recipeGenerator": "Crea un generatore di ricette basato sugli ingredienti disponibili",
"interviewCoach": "Crea un coach di preparazione ai colloqui"
}
},
"notFound": {
"title": "Pagina Non Trovata",
"description": "La pagina che stai cercando non esiste o è stata spostata.",
@@ -676,7 +690,11 @@
"currentPrompt": "In costruzione:",
"stateTitle": "Titolo",
"stateContent": "Contenuto",
"stateTags": "tag"
"stateTags": "tag",
"editAction1": "Compila i campi mancanti, aggiorna i tag.",
"editAction2": "Migliora le variabili",
"editAction3": "Usa le variabili",
"editAction4": "Converti in prompt JSON"
},
"report": {
"report": "Segnala",

View File

@@ -650,6 +650,20 @@
"tokens": "トークン",
"noChanges": "変更なし"
},
"heroPromptInput": {
"placeholder": "作成したいプロンプトを説明してください...",
"ariaLabel": "作成したいプロンプトを説明してください",
"submit": "プロンプトを作成",
"hint": "クリックしてAIで作成を開始",
"modelName": "プロンプトエージェント",
"examples": {
"codeReview": "バグを検出するコードレビューアシスタントを作成",
"emailWriter": "あらゆる場面に対応するプロフェッショナルなメール作成ツールを構築",
"studyPlanner": "パーソナライズされた学習プランジェネレーターを設計",
"recipeGenerator": "手持ちの食材でレシピを作成するツールを作成",
"interviewCoach": "面接対策コーチを作成"
}
},
"notFound": {
"title": "ページが見つかりません",
"description": "お探しのページは存在しないか、移動された可能性があります。",
@@ -676,7 +690,11 @@
"currentPrompt": "構築中:",
"stateTitle": "タイトル",
"stateContent": "内容",
"stateTags": "タグ"
"stateTags": "タグ",
"editAction1": "不足フィールドを埋め、タグを更新。",
"editAction2": "変数を改善",
"editAction3": "変数を使用",
"editAction4": "JSONプロンプトに変換"
},
"report": {
"report": "報告",

View File

@@ -650,6 +650,20 @@
"tokens": "토큰",
"noChanges": "변경 없음"
},
"heroPromptInput": {
"placeholder": "만들고 싶은 프롬프트를 설명하세요...",
"ariaLabel": "만들고 싶은 프롬프트를 설명하세요",
"submit": "프롬프트 만들기",
"hint": "클릭하여 AI로 만들기 시작",
"modelName": "프롬프트 에이전트",
"examples": {
"codeReview": "버그를 잡아내는 코드 리뷰 어시스턴트 만들기",
"emailWriter": "모든 상황에 맞는 전문 이메일 작성기 만들기",
"studyPlanner": "맞춤형 학습 계획 생성기 설계",
"recipeGenerator": "가용 재료로 레시피 생성기 만들기",
"interviewCoach": "면접 준비 코치 만들기"
}
},
"notFound": {
"title": "페이지를 찾을 수 없습니다",
"description": "찾고 계신 페이지가 존재하지 않거나 이동되었습니다.",
@@ -676,7 +690,11 @@
"currentPrompt": "구축 중:",
"stateTitle": "제목",
"stateContent": "내용",
"stateTags": "태그"
"stateTags": "태그",
"editAction1": "누락된 필드 채우기, 태그 업데이트.",
"editAction2": "변수 개선",
"editAction3": "변수 사용",
"editAction4": "JSON 프롬프트로 변환"
},
"report": {
"report": "신고",

View File

@@ -650,6 +650,20 @@
"tokens": "tokens",
"noChanges": "Sem alterações"
},
"heroPromptInput": {
"placeholder": "Descreva o prompt que você quer criar...",
"ariaLabel": "Descreva o prompt que você quer criar",
"submit": "Criar prompt",
"hint": "Clique para começar a criar com IA",
"modelName": "Agente de Prompts",
"examples": {
"codeReview": "Crie um assistente de revisão de código que detecta bugs",
"emailWriter": "Construa um escritor de emails profissionais para qualquer ocasião",
"studyPlanner": "Projete um gerador de planos de estudo personalizados",
"recipeGenerator": "Crie um gerador de receitas baseado em ingredientes disponíveis",
"interviewCoach": "Crie um coach de preparação para entrevistas"
}
},
"notFound": {
"title": "Página Não Encontrada",
"description": "A página que você está procurando não existe ou foi movida.",
@@ -676,7 +690,11 @@
"currentPrompt": "Construindo:",
"stateTitle": "Título",
"stateContent": "Conteúdo",
"stateTags": "tags"
"stateTags": "tags",
"editAction1": "Preencher campos faltantes, atualizar tags.",
"editAction2": "Melhorar variáveis",
"editAction3": "Usar variáveis",
"editAction4": "Converter para prompt JSON"
},
"report": {
"report": "Denunciar",

View File

@@ -650,6 +650,20 @@
"tokens": "token",
"noChanges": "Değişiklik yok"
},
"heroPromptInput": {
"placeholder": "Oluşturmak istediğiniz promptu tanımlayın...",
"ariaLabel": "Oluşturmak istediğiniz promptu tanımlayın",
"submit": "Prompt oluştur",
"hint": "AI ile oluşturmaya başlamak için tıklayın",
"modelName": "Prompt Ajanı",
"examples": {
"codeReview": "Hataları yakalayan bir kod inceleme asistanı oluştur",
"emailWriter": "Her durum için profesyonel e-posta yazıcısı oluştur",
"studyPlanner": "Kişiselleştirilmiş çalışma planı oluşturucu tasarla",
"recipeGenerator": "Mevcut malzemelere göre tarif oluşturucu yap",
"interviewCoach": "Mülakat hazırlık koçu oluştur"
}
},
"notFound": {
"title": "Sayfa Bulunamadı",
"description": "Aradığınız sayfa mevcut değil veya taşınmış olabilir.",
@@ -676,7 +690,11 @@
"currentPrompt": "Oluşturuluyor:",
"stateTitle": "Başlık",
"stateContent": "İçerik",
"stateTags": "etiketler"
"stateTags": "etiketler",
"editAction1": "Eksik alanları doldur, etiketleri güncelle.",
"editAction2": "Değişkenleri iyileştir",
"editAction3": "Değişkenleri kullan",
"editAction4": "JSON promptuna dönüştür"
},
"report": {
"report": "Şikayet Et",

View File

@@ -650,6 +650,20 @@
"tokens": "token",
"noChanges": "无变更"
},
"heroPromptInput": {
"placeholder": "描述你想要创建的提示词...",
"ariaLabel": "描述你想要创建的提示词",
"submit": "创建提示词",
"hint": "点击开始用AI创建",
"modelName": "提示词助手",
"examples": {
"codeReview": "创建一个能发现bug的代码审查助手",
"emailWriter": "打造一个适用于各种场合的专业邮件撰写器",
"studyPlanner": "设计一个个性化学习计划生成器",
"recipeGenerator": "制作一个根据现有食材生成食谱的工具",
"interviewCoach": "创建一个面试准备教练"
}
},
"notFound": {
"title": "页面未找到",
"description": "您要查找的页面不存在或已被移动。",
@@ -676,7 +690,11 @@
"currentPrompt": "构建中:",
"stateTitle": "标题",
"stateContent": "内容",
"stateTags": "标签"
"stateTags": "标签",
"editAction1": "填写缺失字段,更新标签。",
"editAction2": "改进变量",
"editAction3": "使用变量",
"editAction4": "转换为JSON提示词"
},
"report": {
"report": "举报",

1180
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.68.0",
"react-markdown": "^10.1.0",
"sharp": "^0.33.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",

View File

@@ -10,17 +10,23 @@ import {
const GENERATIVE_MODEL = process.env.OPENAI_GENERATIVE_MODEL || "gpt-4o-mini";
const SYSTEM_PROMPT = `You are an expert prompt engineer agent. Your job is to quickly build high-quality prompts.
const SYSTEM_PROMPT = `You are an expert prompt engineer agent. Your job is to quickly build high-quality prompts that match the style and quality of existing prompts in the database.
MANDATORY FIRST STEP - LEARN FROM EXAMPLES:
Before creating ANY prompt, you MUST first call search_prompts to study existing prompts in the database.
This is NON-NEGOTIABLE. You need to understand how prompts are written in this system before creating new ones.
Study the structure, tone, format, and quality of existing prompts and match that style.
IMPORTANT - REASONING FORMAT:
Before EVERY tool call, write a brief reasoning line starting with "→" to explain what you're about to do.
Example: "→ Searching for similar prompts to get inspiration..."
Example: "→ Searching for similar prompts to learn the style..."
This makes your actions transparent and agentic.
WORKFLOW FOR NEW PROMPTS:
1. ALWAYS call search_prompts first to find similar examples for inspiration
2. Immediately set the prompt fields using tools (set_title, set_description, set_content, set_tags)
3. Only respond with a brief summary of what you created
1. MANDATORY: Call search_prompts FIRST to study 3-5 similar examples - learn their structure, tone, and format
2. Analyze the examples to understand how prompts are written in this database
3. Create your prompt matching that same quality and style using tools (set_title, set_description, set_content, set_tags)
4. Only respond with a brief summary of what you created
WORKFLOW FOR CHANGES/EDITS:
1. The current prompt state is provided below - review it first
@@ -52,23 +58,53 @@ If the user's request mentions any of these, automatically call set_media_requir
Set appropriate mediaType (IMAGE, VIDEO, DOCUMENT) based on context.
RULES:
- NEVER skip searching for examples - this is your first action for ANY new prompt
- Be ACTION-ORIENTED: Use tools immediately, don't ask many questions
- ALWAYS write reasoning ("→ ...") before each tool call
- For NEW prompts: search for examples first, then build
- For NEW prompts: MUST search for examples first to learn the style, then build matching that style
- For EDITS: modify only what the user asked, don't rewrite everything
- For JSON: search structured prompts first, then convert preserving all content
- Auto-detect and set media requirements when user mentions files/uploads
- Use variables: \${variableName} or \${variableName:defaultValue} for customizable parts
- Keep responses SHORT - just confirm what you did
- If the request is clear, act immediately
- If the request is clear, act immediately (but always search examples first for new prompts)
- Only ask ONE clarifying question if absolutely necessary
PROMPT STYLE (MANDATORY FOR TEXT PROMPTS):
- ALWAYS use "Act as" role-playing format: "Act as a [role]. You are [description]..."
- Be INSTRUCTIVE and IMPERATIVE: Use "do this", "act as", "you will", "your task is"
- Define a clear ROLE/PERSONA the AI should adopt
- Include specific RESPONSIBILITIES and BEHAVIORS
- Add CONSTRAINTS and RULES for the role
- Example format:
"Act as a [Role]. You are an expert in [domain] with [experience/skills].
Your task is to [main objective].
You will:
- [Responsibility 1]
- [Responsibility 2]
Rules:
- [Constraint 1]
- [Constraint 2]"
VARIABLES (HIGHLY ENCOURAGED):
- ALWAYS look for opportunities to add variables to make prompts reusable
- Use syntax: \${variableName} or \${variableName:defaultValue}
- Common variable patterns:
- \${topic} - main subject/topic
- \${language:English} - target language with default
- \${tone:professional} - writing tone
- \${length:medium} - output length
- \${context} - additional context from user
- \${input} - user's input text to process
- Variables make prompts flexible and powerful - include at least 1-2 in every prompt
- Example: "Translate the following text to \${language:Spanish}"
PROMPT QUALITY:
- Write clear, specific instructions
- Include context and constraints
- Add examples when helpful
- Use structured sections for complex prompts
- Make prompts reusable with variables
- Make prompts reusable with variables (see above)
You have tools to: search_prompts (with promptType and structuredFormat filters), set_title, set_description, set_content, set_type, set_tags, set_category, set_privacy, set_media_requirements, get_current_state, get_available_tags, get_available_categories.

View File

@@ -6,6 +6,7 @@ import { auth } from "@/lib/auth";
import { getConfig } from "@/lib/config";
import { Button } from "@/components/ui/button";
import { DiscoveryPrompts } from "@/components/prompts/discovery-prompts";
import { HeroPromptInput } from "@/components/prompts/hero-prompt-input";
function getOrdinalSuffix(n: number): string {
const s = ["th", "st", "nd", "rd"];
@@ -24,6 +25,7 @@ export default async function HomePage() {
const showRegisterButton = !session && (isOAuth || (config.auth.provider === "credentials" && config.auth.allowRegistration));
const useCloneBranding = config.homepage?.useCloneBranding ?? false;
const aiGenerationEnabled = config.features?.aiGeneration ?? false;
// Fetch GitHub stars dynamically (with caching) - only if not using clone branding
let githubStars = 139000; // fallback
@@ -67,16 +69,25 @@ export default async function HomePage() {
</div>
) : (
<div className="absolute top-0 right-0 bottom-0 w-1/2 hidden md:block pointer-events-none">
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/80 to-transparent z-10" />
<video
autoPlay
loop
muted
playsInline
className="absolute top-1/2 -translate-y-1/2 right-0 w-full h-auto opacity-30 dark:opacity-15 dark:invert"
>
<source src="/animation.mp4" type="video/mp4" />
</video>
{/* Video background */}
<div className="absolute inset-0">
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/80 to-transparent z-10" />
<video
autoPlay
loop
muted
playsInline
className="absolute top-1/2 -translate-y-1/2 right-0 w-full h-auto opacity-30 dark:opacity-15 dark:invert"
>
<source src="/animation.mp4" type="video/mp4" />
</video>
</div>
{/* Animated input overlay - only show if AI generation is enabled */}
{aiGenerationEnabled && (
<div className="absolute inset-0 hidden lg:flex items-center justify-center z-30 pr-8 pointer-events-auto">
<HeroPromptInput />
</div>
)}
</div>
)}

View File

@@ -11,9 +11,14 @@ export const metadata: Metadata = {
description: "Create a new prompt",
};
export default async function NewPromptPage() {
interface PageProps {
searchParams: Promise<{ prompt?: string }>;
}
export default async function NewPromptPage({ searchParams }: PageProps) {
const session = await auth();
const t = await getTranslations("prompts");
const { prompt: initialPromptRequest } = await searchParams;
if (!session?.user) {
redirect("/login");
@@ -46,6 +51,7 @@ export default async function NewPromptPage() {
tags={tags}
aiGenerationEnabled={aiGenerationEnabled}
aiModelName={aiModelName}
initialPromptRequest={initialPromptRequest}
/>
</div>
);

View File

@@ -0,0 +1,185 @@
"use client";
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Bot, ArrowUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const TYPING_SPEED = 50; // ms per character
const PAUSE_BETWEEN_PROMPTS = 2000; // ms to pause after completing a prompt
const DELETE_SPEED = 30; // ms per character when deleting
export function HeroPromptInput() {
const t = useTranslations("heroPromptInput");
const router = useRouter();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const examplePrompts = useMemo(() => [
t("examples.codeReview"),
t("examples.emailWriter"),
t("examples.studyPlanner"),
t("examples.recipeGenerator"),
t("examples.interviewCoach"),
], [t]);
const [displayText, setDisplayText] = useState("");
const [inputValue, setInputValue] = useState("");
const [isFocused, setIsFocused] = useState(false);
const [isAnimating, setIsAnimating] = useState(true);
const [currentPromptIndex, setCurrentPromptIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false);
const animationRef = useRef<NodeJS.Timeout | null>(null);
const clearAnimation = useCallback(() => {
if (animationRef.current) {
clearTimeout(animationRef.current);
animationRef.current = null;
}
}, []);
// Typing animation effect
useEffect(() => {
if (!isAnimating || isFocused) {
clearAnimation();
return;
}
const currentPrompt = examplePrompts[currentPromptIndex];
if (isDeleting) {
if (displayText.length > 0) {
animationRef.current = setTimeout(() => {
setDisplayText((prev) => prev.slice(0, -1));
}, DELETE_SPEED);
} else {
setIsDeleting(false);
setCurrentPromptIndex((prev) => (prev + 1) % examplePrompts.length);
}
} else {
if (displayText.length < currentPrompt.length) {
animationRef.current = setTimeout(() => {
setDisplayText(currentPrompt.slice(0, displayText.length + 1));
}, TYPING_SPEED);
} else {
// Finished typing, wait then start deleting
animationRef.current = setTimeout(() => {
setIsDeleting(true);
}, PAUSE_BETWEEN_PROMPTS);
}
}
return clearAnimation;
}, [displayText, isAnimating, isFocused, currentPromptIndex, isDeleting, clearAnimation, examplePrompts]);
const handleFocus = () => {
setIsFocused(true);
setIsAnimating(false);
clearAnimation();
// Transfer the animated text to the actual input value
setInputValue(displayText);
setDisplayText("");
};
const handleBlur = () => {
setIsFocused(false);
// Only restart animation if input is empty
if (!inputValue.trim()) {
setIsAnimating(true);
}
};
const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
const value = inputValue.trim();
if (value) {
router.push(`/prompts/new?prompt=${encodeURIComponent(value)}`);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
const handleAnimatedTextClick = () => {
// Stop animation, clear input, and focus for user to type
setIsFocused(true);
setIsAnimating(false);
clearAnimation();
setInputValue("");
setDisplayText("");
// Focus the textarea
setTimeout(() => {
textareaRef.current?.focus();
}, 0);
};
return (
<form onSubmit={handleSubmit} className="w-full max-w-lg">
<div
className={cn(
"rounded-xl bg-muted/50 border px-4 py-3 backdrop-blur-sm transition-all duration-200 shadow-sm",
isFocused && "border-foreground/30 ring-1 ring-ring"
)}
>
{/* Textarea area with animated text overlay */}
<div className="relative min-h-[44px]">
{/* Animated placeholder text - clickable to redirect */}
{!isFocused && isAnimating && (
<button
type="button"
onClick={handleAnimatedTextClick}
className="absolute inset-0 flex items-start text-left cursor-pointer hover:opacity-80 transition-opacity"
>
<span className="text-base text-muted-foreground">
{displayText}
<span className="inline-block w-0.5 h-5 bg-primary ml-0.5 animate-pulse align-middle" />
</span>
</button>
)}
{/* Actual textarea */}
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={isFocused ? t("placeholder") : ""}
className={cn(
"min-h-[44px] max-h-[100px] w-full resize-none text-base bg-transparent border-0 p-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none placeholder:text-muted-foreground",
!isFocused && isAnimating && "text-transparent caret-transparent pointer-events-none"
)}
aria-label={t("ariaLabel")}
/>
</div>
{/* Bottom row: Bot icon + model name + submit button */}
<div className="flex items-center justify-between mt-2">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Bot className="h-3 w-3" />
<span>{t("modelName")}</span>
</div>
<Button
type="submit"
size="icon"
disabled={!inputValue.trim()}
className="h-7 w-7 rounded-full"
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<p className="text-sm text-muted-foreground mt-3 text-center">
{t("hint")}
</p>
</form>
);
}

View File

@@ -3,6 +3,7 @@
import { useState, useRef, useEffect } from "react";
import { useTranslations } from "next-intl";
import { ArrowUp, Loader2, Sparkles, X, ChevronRight, Bot } from "lucide-react";
import ReactMarkdown from "react-markdown";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
@@ -55,6 +56,7 @@ interface PromptBuilderProps {
currentState: PromptBuilderState;
onStateChange: (state: PromptBuilderState) => void;
modelName?: string;
initialPromptRequest?: string;
}
export function PromptBuilder({
@@ -63,6 +65,7 @@ export function PromptBuilder({
currentState,
onStateChange,
modelName = "gpt-4o-mini",
initialPromptRequest,
}: PromptBuilderProps) {
const t = useTranslations("promptBuilder");
const [isOpen, setIsOpen] = useState(true);
@@ -82,13 +85,26 @@ export function PromptBuilder({
scrollToBottom();
}, [messages, streamingContent]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
// Auto-send initial prompt request if provided
const initialRequestSentRef = useRef(false);
useEffect(() => {
if (initialPromptRequest && !initialRequestSentRef.current && !isLoading) {
initialRequestSentRef.current = true;
setInput(initialPromptRequest);
// Use setTimeout to ensure the input is set before sending
setTimeout(() => {
sendMessageWithContent(initialPromptRequest);
}, 100);
}
}, [initialPromptRequest]);
const sendMessageWithContent = async (content: string) => {
if (!content.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: "user",
content: input.trim(),
content: content.trim(),
};
setMessages((prev) => [...prev, userMessage]);
@@ -198,6 +214,11 @@ export function PromptBuilder({
}
};
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
await sendMessageWithContent(input);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
@@ -320,12 +341,7 @@ export function PromptBuilder({
{/* Show edit mode actions if there's existing content */}
{currentState.content ? (
<>
{[
"Fill missing fields, update tags.",
"Make variables better",
"Use variables",
"Convert to JSON prompt"
].map((action, i) => (
{[t("editAction1"), t("editAction2"), t("editAction3"), t("editAction4")].map((action, i) => (
<button
type="button"
key={i}
@@ -378,7 +394,40 @@ export function PromptBuilder({
: "bg-muted"
)}
>
<p className="whitespace-pre-wrap">{message.content}</p>
{message.role === "user" ? (
<p className="whitespace-pre-wrap">{message.content}</p>
) : (
<div className="text-sm">
<ReactMarkdown
components={{
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
ul: ({ children }) => <ul className="mb-2 ml-4 list-disc list-inside">{children}</ul>,
ol: ({ children }) => <ol className="mb-2 ml-4 list-decimal list-inside">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,
code: ({ className, children, ...props }) => {
const isBlock = className?.includes("language-") || String(children).includes("\n");
if (isBlock) {
return (
<pre className="bg-background/80 border rounded-md p-3 my-2 overflow-x-auto">
<code className="text-xs">{children}</code>
</pre>
);
}
return <code className="bg-background/80 px-1.5 py-0.5 rounded text-xs font-mono" {...props}>{children}</code>;
},
pre: ({ children }) => <>{children}</>,
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
h1: ({ children }) => <h1 className="text-lg font-bold mb-2 mt-3">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-bold mb-2 mt-3">{children}</h2>,
h3: ({ children }) => <h3 className="text-sm font-bold mb-1 mt-2">{children}</h3>,
blockquote: ({ children }) => <blockquote className="border-l-2 border-muted-foreground/50 pl-3 my-2 italic">{children}</blockquote>,
}}
>
{message.content}
</ReactMarkdown>
</div>
)}
</div>
{/* Tool calls indicator */}

View File

@@ -234,9 +234,10 @@ interface PromptFormProps {
mode?: "create" | "edit";
aiGenerationEnabled?: boolean;
aiModelName?: string;
initialPromptRequest?: string;
}
export function PromptForm({ categories, tags, initialData, initialContributors = [], promptId, mode = "create", aiGenerationEnabled = false, aiModelName }: PromptFormProps) {
export function PromptForm({ categories, tags, initialData, initialContributors = [], promptId, mode = "create", aiGenerationEnabled = false, aiModelName, initialPromptRequest }: PromptFormProps) {
const router = useRouter();
const t = useTranslations("prompts");
const tCommon = useTranslations("common");
@@ -422,6 +423,7 @@ export function PromptForm({ categories, tags, initialData, initialContributors
currentState={currentBuilderState}
onStateChange={handleBuilderStateChange}
modelName={aiModelName}
initialPromptRequest={initialPromptRequest}
/>
)}
<FormField

View File

@@ -312,7 +312,7 @@ export async function executeToolCall(
type: string;
structuredFormat: string | null;
tags: string[];
source: "text" | "semantic";
source: "text" | "semantic" | "random";
similarity?: string;
}> = [];
@@ -354,7 +354,45 @@ export async function executeToolCall(
}
// Limit final results
const finalResults = combinedResults.slice(0, limit);
let finalResults = combinedResults.slice(0, limit);
// If no results found, get random prompts to learn the style
if (finalResults.length === 0) {
const randomPrompts = await db.prompt.findMany({
where: {
isPrivate: false,
deletedAt: null,
},
select: {
id: true,
title: true,
description: true,
content: true,
type: true,
structuredFormat: true,
tags: {
select: {
tag: {
select: { name: true, color: true }
}
}
}
},
take: limit,
orderBy: { createdAt: "desc" }
});
finalResults = randomPrompts.map(p => ({
id: p.id,
title: p.title,
description: p.description,
contentPreview: p.content.substring(0, 200) + (p.content.length > 200 ? "..." : ""),
type: p.type,
structuredFormat: p.structuredFormat,
tags: p.tags.map((t: { tag: { name: string } }) => t.tag.name),
source: "random" as const
}));
}
return {
result: {
@@ -362,8 +400,13 @@ export async function executeToolCall(
data: {
prompts: finalResults,
count: finalResults.length,
searchType: useSemanticSearch ? "hybrid" : "text",
filters: { promptType, structuredFormat }
searchType: finalResults.length > 0 && finalResults[0].source === "random"
? "random_examples"
: (useSemanticSearch ? "hybrid" : "text"),
filters: { promptType, structuredFormat },
note: finalResults.length > 0 && finalResults[0].source === "random"
? "No matching prompts found. Showing random examples to understand the prompt style."
: undefined
}
},
newState

View File

@@ -4,14 +4,15 @@ import { LOCALE_COOKIE } from "./config";
/**
* Set the user's locale preference (client-side)
* This updates the cookie and reloads the page
* This updates the cookie and forces a hard navigation to apply the new locale
*/
export function setLocale(locale: string): void {
// Set cookie with 1 year expiry
document.cookie = `${LOCALE_COOKIE}=${locale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
// Reload to apply the new locale
window.location.reload();
// Force a hard navigation to the same URL to ensure server re-renders with new locale
// Using href assignment instead of reload() for more reliable behavior in Next.js
window.location.href = window.location.href;
}
/**