diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index 10413f6..34d1c5f 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -1,16 +1,18 @@ import { NextResponse } from "next/server" -import { rateLimitByIp, RateLimitError } from "@/lib/rate-limit"; +import { + rateLimitByIp, + RateLimitError, + RateLimitUnavailableError, +} from "@/lib/rate-limit"; import { generateBioWithLLM, SupportedModel } from "@/lib/llm-provider"; +import { + generateBioRequestSchema, + MAX_ABOUT_YOU_LENGTH, + normalizeGeneratedBio, +} from "@/lib/generation"; const LLM_MODEL = (process.env.NEXT_LLM_MODEL || "gpt-4o") as SupportedModel -/** Allowed platform values – prevents injection via platform field */ -const ALLOWED_PLATFORMS = ["instagram", "twitter", "linkedin", "tiktok", "telegram", "youtube"] as const; -/** Allowed tone values – prevents injection via tone field */ -const ALLOWED_TONES = ["professional", "friendly", "creative", "humorous"] as const; - -const MAX_ABOUT_YOU_LENGTH = 2000; - /** * Sanitizes user-provided text: truncates length, strips control chars, normalizes whitespace. * Reduces prompt-injection and token-abuse risk. @@ -51,8 +53,6 @@ const BIO_PROMPT_TEMPLATE = `تو یک متخصص نویسنده بیوگراف - twitter: کوتاه و موجز، مناسب برای اظهارنظر - linkedin: حرفه‌ای و تجاری - telegram: ارتباطی و اطلاع‌رسانی -- tiktok: سرگرمی و خلاقیت -- youtube: محتوای ویدیویی و کانال قوانین خروجی: - محدودیت کاراکتر پلتفرم را رعایت کن و بیوگرافی را به زبان فارسی بنویس. @@ -60,29 +60,22 @@ const BIO_PROMPT_TEMPLATE = `تو یک متخصص نویسنده بیوگراف - این کلمات را در بیوگرافی استفاده نکن: احسان، عین، عین الله، غفار.`; export async function POST(request: Request) { - try { - await rateLimitByIp(request); + let body: unknown; - const body = await request.json(); - const rawAbout = body?.aboutYou; - const platform = typeof body?.platform === "string" ? body.platform.toLowerCase().trim() : ""; - const tone = typeof body?.tone === "string" ? body.tone.toLowerCase().trim() : ""; + try { + body = await request.json(); + const parsed = generateBioRequestSchema.safeParse(body); - if (!rawAbout || !platform || !tone) { - return NextResponse.json({ error: "لطفاً تمام فیلدهای مورد نیاز را پر کنید." }, { status: 400 }); + if (!parsed.success) { + return NextResponse.json( + { error: "لطفاً اطلاعات ورودی را به‌درستی تکمیل کنید." }, + { status: 400 }, + ); } - if (!ALLOWED_PLATFORMS.includes(platform as (typeof ALLOWED_PLATFORMS)[number])) { - return NextResponse.json({ error: "پلتفرم انتخاب‌شده معتبر نیست." }, { status: 400 }); - } - if (!ALLOWED_TONES.includes(tone as (typeof ALLOWED_TONES)[number])) { - return NextResponse.json({ error: "لحن انتخاب‌شده معتبر نیست." }, { status: 400 }); - } + await rateLimitByIp(request); - const aboutYou = sanitizeUserInput(rawAbout); - if (!aboutYou) { - return NextResponse.json({ error: "محتوای «درباره من» معتبر نیست یا خالی است." }, { status: 400 }); - } + const { aboutYou, platform, tone } = parsed.data; const result = await generateBioWithLLM(LLM_MODEL, BIO_PROMPT_TEMPLATE, { aboutYou, @@ -90,35 +83,38 @@ export async function POST(request: Request) { tone, }) - console.log("Generated bio:", result); + const generatedBio = normalizeGeneratedBio(result.text, platform); - // Extract the generated bio from the result - const generatedBio = result.text - - // Return the generated bio - return NextResponse.json({ - bio: generatedBio, - model: LLM_MODEL, - }) + return NextResponse.json( + { bio: generatedBio, model: LLM_MODEL }, + { headers: { "Cache-Control": "no-store" } }, + ); } catch (error) { - console.error("Error in generate-bio API:", error) + console.error("Error in generate-bio API:", error instanceof Error ? error.name : "unknown"); if (error instanceof RateLimitError) { return NextResponse.json( { error: error.message }, - { status: 429 } + { status: 429, headers: { "Cache-Control": "no-store" } }, + ) + } + + if (error instanceof RateLimitUnavailableError) { + return NextResponse.json( + { error: "سرویس محدودسازی درخواست موقتاً در دسترس نیست. لطفاً بعداً دوباره تلاش کنید." }, + { status: 503, headers: { "Cache-Control": "no-store" } }, ) } try { - const body = await request.json() - const fallbackBio = generateFallbackBio(body.aboutYou, body.platform, body.tone) + const parsed = generateBioRequestSchema.parse(body); + const fallbackBio = generateFallbackBio(parsed.aboutYou, parsed.platform, parsed.tone); return NextResponse.json( { - bio: fallbackBio, + bio: normalizeGeneratedBio(fallbackBio, parsed.platform), note: "تولید شده با سیستم پشتیبان به دلیل مشکل در ارتباط با هوش مصنوعی", }, - { status: 200 } + { status: 200, headers: { "Cache-Control": "no-store" } }, ) } catch (fallbackError) { return NextResponse.json( diff --git a/app/page.tsx b/app/page.tsx index 959c5f3..3491ef5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -30,7 +30,6 @@ export default function BioGenerator() { const [generatedBio, setGeneratedBio] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [copied, setCopied] = useState(false); - const [charCount, setCharCount] = useState(0); const [note, setNote] = useState(""); const [error, setError] = useState(""); const [isCooldown, setIsCooldown] = useState(false); @@ -41,10 +40,6 @@ export default function BioGenerator() { } }, []); - useEffect(() => { - setCharCount(aboutYou.length); - }, [aboutYou]); - useEffect(() => { let interval: ReturnType; if (isCooldown && cooldownTimer > 0) { @@ -57,6 +52,13 @@ export default function BioGenerator() { return () => clearInterval(interval); }, [isCooldown, cooldownTimer]); + useEffect(() => { + if (!copied) return; + + const timeout = setTimeout(() => setCopied(false), 2000); + return () => clearTimeout(timeout); + }, [copied]); + const getCurrentPlatform = () => { return ( platforms.find((p) => p.value === platform) || { @@ -67,14 +69,17 @@ export default function BioGenerator() { const getCharLimitPercent = () => { const currentLimit = getCurrentPlatform().limit; - return (charCount / currentLimit) * 100; + return (aboutYou.length / currentLimit) * 100; }; - const copyToClipboard = () => { - navigator.clipboard.writeText(generatedBio); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - sonnar("بایو کپی شد", { icon: "✂️" }); + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(generatedBio); + setCopied(true); + sonnar("بایو کپی شد", { icon: "✂️" }); + } catch { + sonnar.error("کپی کردن بایو ممکن نشد."); + } }; const generateBio = async () => { @@ -202,6 +207,7 @@ export default function BioGenerator() { {platform && ( 90 ? "bg-destructive/10 text-destructive" @@ -210,25 +216,30 @@ export default function BioGenerator() { : "bg-emerald-500/10 text-emerald-600" }`} > - {charCount} / {getCurrentPlatform().limit} + {aboutYou.length} / {getCurrentPlatform().limit} )} +