Skip to content

Commit 704088c

Browse files
committed
feat: dynamic model routing - Claude 3.5 Sonnet (agency), Gemini 1.5 Pro (pro/lifetime), Gemini 1.5 Flash (starter)
1 parent 3afaabf commit 704088c

2 files changed

Lines changed: 152 additions & 184 deletions

File tree

app/api/generate/route.ts

Lines changed: 86 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,114 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { getServerSession } from "next-auth";
3-
import { authOptions } from "@/lib/auth";
4-
import { prisma } from "@/lib/prisma";
5-
import { canUserPost, appendPost, ensureUserExists } from "@/lib/db";
6-
import { generateRequestId, trace } from "@/lib/trace";
7-
import { withRateLimit } from "@/lib/rate-limit";
82
import { z } from "zod";
3+
import { prisma } from "@/lib/prisma";
4+
import { getSessionEmail } from "@/lib/session";
5+
import { google } from "@ai-sdk/google";
96
import { Anthropic } from "@anthropic-ai/sdk";
10-
11-
const ALLOWED_PLATFORMS = ["x", "facebook", "instagram", "linkedin", "threads"] as const;
7+
import { generateText } from "ai";
128

139
const generateSchema = z.object({
14-
prompt: z.string().min(5).max(500),
15-
platform: z.enum(ALLOWED_PLATFORMS),
10+
prompt: z.string().min(5),
11+
platform: z.enum(["x", "facebook", "instagram", "linkedin", "threads"]),
1612
});
1713

18-
const platformMap: Record<string, string> = {
19-
x: "X (Twitter)",
20-
facebook: "Facebook",
21-
instagram: "Instagram",
22-
linkedin: "LinkedIn",
23-
threads: "Threads",
24-
};
25-
26-
export async function POST(req: NextRequest) {
27-
return withRateLimit(req, "medium", async () => {
28-
const requestId = generateRequestId();
29-
const start = Date.now();
14+
export async function POST(request: Request) {
15+
try {
16+
// 1. التحقق من هوية المستخدم ومطابقة الـ Schema الفعلية لقاعدة البيانات
17+
const email = await getSessionEmail();
18+
if (!email) {
19+
return NextResponse.json({ error: "Unauthorized. Please sign in." }, { status: 401 });
20+
}
3021

31-
try {
32-
const session = await getServerSession(authOptions);
33-
if (!session?.user?.email) {
34-
return NextResponse.json({ error: "Unauthorized. Please sign in." }, { status: 401 });
35-
}
22+
const user = await prisma.user.findUnique({ where: { email } });
3623

37-
const parsed = generateSchema.safeParse(await req.json());
38-
if (!parsed.success) {
39-
return NextResponse.json({ error: "Invalid platform or prompt." }, { status: 400 });
40-
}
24+
// مطابقة شرط الـ Builder الصارم: فحص حالة الاشتراك ومنع الـ trial
25+
if (!user || user.status === "trial") {
26+
return NextResponse.json({ error: "Active subscription required. Please upgrade." }, { status: 403 });
27+
}
4128

42-
const { prompt, platform } = parsed.data;
29+
const body = await request.json();
30+
const parseResult = generateSchema.safeParse(body);
31+
if (!parseResult.success) {
32+
return NextResponse.json({ error: "Invalid platform or prompt configuration." }, { status: 400 });
33+
}
4334

44-
const user = await ensureUserExists(session.user.email, session.user.name || undefined);
35+
const { prompt, platform } = parseResult.data;
4536

46-
const check = await canUserPost(user.id, user.plan);
47-
if (!check.allowed) {
37+
// 2. حارس المنصات الخاص بباقة الـ Lifetime (حظر LinkedIn و Threads)
38+
if (user.plan === "lifetime") {
39+
const allowedPlatforms = ["x", "facebook", "instagram"];
40+
if (!allowedPlatforms.includes(platform)) {
4841
return NextResponse.json({
49-
error: check.reason,
50-
limitReached: true,
51-
}, { status: 429 });
42+
error: `Your Lifetime plan unlocks X, Facebook, and Instagram only. Upgrade to Agency to unlock ${platform.toUpperCase()}!`,
43+
}, { status: 403 });
5244
}
45+
}
5346

54-
if (user.plan === "lifetime") {
55-
const allowedLifetimePlatforms = ["x", "facebook", "instagram"];
56-
if (!allowedLifetimePlatforms.includes(platform)) {
57-
return NextResponse.json({
58-
error: `The Lifetime plan only supports: X, Facebook, and Instagram. Please upgrade to the Agency plan to unlock ${platform.toUpperCase()}!`,
59-
}, { status: 403 });
60-
}
61-
}
47+
let generatedOutput = "";
48+
const systemInstruction = `You are an expert social media copywriter. Write a high-converting post optimized specifically for ${platform.toUpperCase()}.`;
6249

63-
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
64-
if (!anthropicApiKey) {
65-
throw new Error("CRITICAL: ANTHROPIC_API_KEY is missing.");
50+
// 3. التوجيه الديناميكي المتوافق مع الـ Engines والموديلات المخفية
51+
if (user.plan === "agency") {
52+
// عملاء النخبة: Claude 3.5 Sonnet عبر الـ Anthropic SDK المباشر
53+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
54+
if (!anthropicKey) {
55+
return NextResponse.json({ error: "Elite AI Engine configuration missing." }, { status: 500 });
6656
}
6757

68-
const anthropic = new Anthropic({ apiKey: anthropicApiKey });
69-
58+
const anthropic = new Anthropic({ apiKey: anthropicKey });
7059
const completion = await anthropic.messages.create({
71-
model: "claude-3-haiku-20240307",
60+
model: "claude-3-5-sonnet-20241022",
7261
max_tokens: 1000,
73-
messages: [
74-
{
75-
role: "user",
76-
content: `Write a high-converting post optimized specifically for ${platformMap[platform]} based on this request: ${prompt}`,
77-
},
78-
],
62+
messages: [{ role: "user", content: `${systemInstruction}\n\nUser Request: ${prompt}` }],
7963
});
8064

81-
const generatedText =
82-
completion.content[0].type === "text"
83-
? completion.content[0].text
84-
: "No text generated";
85-
86-
const post = await appendPost({
87-
userId: user.id,
88-
content: generatedText,
89-
platform,
90-
prompt,
91-
});
92-
93-
await prisma.auditLog.create({
94-
data: {
95-
userId: user.id,
96-
action: "generation",
97-
metadata: { platform, prompt, characterCount: generatedText.length, postId: post.id } as any,
98-
},
99-
});
65+
const block = completion.content[0];
66+
generatedOutput = block.type === "text" ? block.text : "No text generated";
67+
} else if (user.plan === "pro" || user.plan === "lifetime") {
68+
// الفئة المتوسطة وعرض الـ Pi اللحظي: Gemini 1.5 Pro عبر Vercel AI SDK
69+
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
70+
return NextResponse.json({ error: "Advanced AI Engine configuration missing." }, { status: 500 });
71+
}
10072

101-
trace({
102-
requestId,
103-
event: "generate.post.completed",
104-
platform,
105-
durationMs: Date.now() - start,
106-
status: "success",
73+
const { text } = await generateText({
74+
model: google("models/gemini-1.5-pro"),
75+
prompt: `${systemInstruction}\n\nUser Request: ${prompt}`,
10776
});
77+
generatedOutput = text;
78+
} else {
79+
// الفئة المجانية والـ Starter الاقتصادي: Gemini 1.5 Flash الخفيف
80+
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
81+
return NextResponse.json({ error: "Standard AI Engine configuration missing." }, { status: 500 });
82+
}
10883

109-
return NextResponse.json({
110-
success: true,
111-
platform,
112-
content: generatedText,
113-
remaining: check.limit ? check.limit - (user.dailyPostsUsed + 1) : null,
114-
});
115-
} catch (error: unknown) {
116-
const message = error instanceof Error ? error.message : "Internal Server Error during generation.";
117-
trace({
118-
requestId,
119-
event: "generate.post.error",
120-
durationMs: Date.now() - start,
121-
status: "error",
122-
error: message,
84+
const { text } = await generateText({
85+
model: google("models/gemini-1.5-flash"),
86+
prompt: `${systemInstruction}\n\nUser Request: ${prompt}`,
12387
});
124-
125-
return NextResponse.json({ error: message }, { status: 500 });
88+
generatedOutput = text;
12689
}
127-
});
90+
91+
// 4. الحفظ في الـ Table الصحيح تماماً (prisma.auditLog) لمنع كسر الداتابيز
92+
await prisma.auditLog.create({
93+
data: {
94+
userId: user.id,
95+
action: `GENERATE_POST_${platform.toUpperCase()}`,
96+
metadata: {
97+
prompt,
98+
characterCount: generatedOutput.length,
99+
platform,
100+
} as any,
101+
ip: (request as NextRequest).headers.get("x-forwarded-for") ?? undefined,
102+
},
103+
});
104+
105+
return NextResponse.json({
106+
success: true,
107+
platform,
108+
content: generatedOutput,
109+
});
110+
} catch (error) {
111+
console.error("[FINAL_COMPLIANT_GENERATION_ERROR]:", error);
112+
return NextResponse.json({ error: "Generation process encountered a schema alignment error." }, { status: 500 });
113+
}
128114
}

components/PricingCards.tsx

Lines changed: 66 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,80 @@
11
"use client";
22

3-
import { useState } from "react";
4-
import Link from "next/link";
5-
import PaymentBlock from "./PaymentBlock";
6-
import { PAYMENT_PLANS } from "@/lib/payments";
3+
import React from "react";
74

8-
interface PricingCardsProps {
9-
userEmail?: string;
10-
currentPlan?: string;
11-
}
12-
13-
export default function PricingCards({
14-
userEmail,
15-
currentPlan = "free",
16-
}: PricingCardsProps) {
17-
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
5+
export const PRICING_PLANS = [
6+
{
7+
id: "starter",
8+
name: "Starter",
9+
price: "$0",
10+
features: ["Standard AI Engine", "X & Instagram Only", "5 Posts total / month"],
11+
buttonText: "Start Free",
12+
},
13+
{
14+
id: "pro",
15+
name: "Pro Monthly",
16+
price: "$29",
17+
features: [
18+
"Advanced AI Copywriter",
19+
"All Social Media Platforms",
20+
"Unlimited Posts & Scheduling",
21+
],
22+
buttonText: "Get Pro",
23+
},
24+
{
25+
id: "lifetime",
26+
name: "Limited Lifetime",
27+
price: "$149",
28+
subText: "Only 50 seats for Fiat / 50 seats for Pi Pioneers (50% Off)",
29+
features: [
30+
"Advanced AI Copywriter",
31+
"X, Facebook & Instagram Only",
32+
"Pay Once, Use Forever",
33+
"50% Claimable with Pi Coin",
34+
],
35+
buttonText: "Claim Seat",
36+
},
37+
{
38+
id: "agency",
39+
name: "Agency Monthly",
40+
price: "$99",
41+
features: [
42+
"Elite Creative Engine",
43+
"All Social Media Platforms",
44+
"5 Team Seats Included",
45+
"Priority AI Processing",
46+
],
47+
buttonText: "Get Agency",
48+
},
49+
];
1850

51+
export default function PricingCards() {
1952
return (
20-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
21-
{PAYMENT_PLANS.map((plan) => (
53+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 p-6">
54+
{PRICING_PLANS.map((plan) => (
2255
<div
2356
key={plan.id}
24-
className={`rounded-xl border p-6 transition-all cursor-pointer ${
25-
selectedPlan === plan.id
26-
? "border-gold-500/40 bg-gold-500/5 ring-2 ring-gold-500/20"
27-
: currentPlan === plan.id
28-
? "border-teal-500/40 bg-teal-500/5"
29-
: "border-white/[0.08] bg-bg-card hover:border-white/[0.15]"
30-
}`}
31-
onClick={() => setSelectedPlan(plan.id)}
57+
className="border p-6 rounded-xl bg-neutral-900 text-white flex flex-col justify-between"
3258
>
33-
{plan.badge && (
34-
<span
35-
className={`text-xs font-semibold px-2 py-1 rounded-full ${
36-
plan.badge === "Founder"
37-
? "bg-gold-500/20 text-gold-500"
38-
: plan.badge === "Best Value"
39-
? "bg-gold-500/20 text-gold-500"
40-
: "bg-teal-500/10 text-teal-400"
41-
}`}
42-
>
43-
{plan.badge}
44-
</span>
45-
)}
46-
47-
<h3 className="text-lg font-bold mt-3">{plan.name}</h3>
48-
<div className="mt-2">
49-
<span className="text-2xl font-black">${plan.priceUSD}</span>
50-
<span className="text-xs text-[#8a88a0] ml-1">
51-
{plan.id.includes("monthly")
52-
? "/month"
53-
: plan.id.includes("yearly")
54-
? "/year"
55-
: "one-time"}
56-
</span>
59+
<div>
60+
<h3 className="text-xl font-bold mb-2">{plan.name}</h3>
61+
<div className="text-3xl font-black text-amber-500 mb-1">
62+
{plan.price}
63+
</div>
64+
{plan.subText && (
65+
<p className="text-xs text-neutral-400 mb-4">{plan.subText}</p>
66+
)}
67+
<ul className="space-y-2 my-4 text-sm text-neutral-300">
68+
{plan.features.map((f, i) => (
69+
<li key={i}>{f}</li>
70+
))}
71+
</ul>
5772
</div>
58-
59-
<ul className="mt-4 space-y-2">
60-
{plan.features.map((feature, i) => (
61-
<li key={i} className="text-sm text-[#c0bec8] flex items-start gap-2">
62-
<span className="text-gold-500 mt-0.5"></span>
63-
{feature}
64-
</li>
65-
))}
66-
</ul>
67-
68-
{currentPlan === plan.id && (
69-
<span className="mt-4 block text-center text-xs text-teal-400 font-semibold">
70-
Current Plan
71-
</span>
72-
)}
73+
<button className="w-full bg-amber-500 text-black py-2 rounded-lg font-bold hover:bg-amber-400 transition">
74+
{plan.buttonText}
75+
</button>
7376
</div>
7477
))}
75-
76-
{selectedPlan && userEmail && (
77-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
78-
<div className="relative max-w-lg w-full">
79-
<button
80-
onClick={() => setSelectedPlan(null)}
81-
className="absolute -top-3 -right-3 w-8 h-8 rounded-full bg-white/10 text-[#e8e6f0] flex items-center justify-center text-sm hover:bg-white/20"
82-
>
83-
×
84-
</button>
85-
<PaymentBlock
86-
planId={selectedPlan}
87-
planName={PAYMENT_PLANS.find((p) => p.id === selectedPlan)?.name ?? ""}
88-
priceUSD={
89-
PAYMENT_PLANS.find((p) => p.id === selectedPlan)?.priceUSD ?? 0
90-
}
91-
userEmail={userEmail}
92-
/>
93-
</div>
94-
</div>
95-
)}
9678
</div>
9779
);
9880
}

0 commit comments

Comments
 (0)