route.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { streamText } from "ai";
  2. import { headers } from "next/headers";
  3. import { buildUserPrompt } from "@json-render/core";
  4. import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
  5. import { playgroundCatalog } from "@/lib/render/catalog";
  6. export const maxDuration = 30;
  7. const SYSTEM_PROMPT = playgroundCatalog.prompt({
  8. customRules: [
  9. "NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
  10. "NEVER use page background colors (bg-gray-50) - the container has its own background.",
  11. "For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
  12. "For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
  13. "Wrap each repeated item in a Card for visual separation and structure.",
  14. "Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
  15. ],
  16. });
  17. const MAX_PROMPT_LENGTH = 500;
  18. const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
  19. export async function POST(req: Request) {
  20. // Get client IP for rate limiting
  21. const headersList = await headers();
  22. const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
  23. // Check rate limits (minute and daily)
  24. const [minuteResult, dailyResult] = await Promise.all([
  25. minuteRateLimit.limit(ip),
  26. dailyRateLimit.limit(ip),
  27. ]);
  28. if (!minuteResult.success || !dailyResult.success) {
  29. const isMinuteLimit = !minuteResult.success;
  30. return new Response(
  31. JSON.stringify({
  32. error: "Rate limit exceeded",
  33. message: isMinuteLimit
  34. ? "Too many requests. Please wait a moment before trying again."
  35. : "Daily limit reached. Please try again tomorrow.",
  36. }),
  37. {
  38. status: 429,
  39. headers: { "Content-Type": "application/json" },
  40. },
  41. );
  42. }
  43. const { prompt, context } = await req.json();
  44. const userPrompt = buildUserPrompt({
  45. prompt,
  46. currentSpec: context?.previousSpec,
  47. maxPromptLength: MAX_PROMPT_LENGTH,
  48. });
  49. const result = streamText({
  50. model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
  51. system: SYSTEM_PROMPT,
  52. prompt: userPrompt,
  53. temperature: 0.7,
  54. });
  55. // Stream the text, then append token usage metadata at the end
  56. const encoder = new TextEncoder();
  57. const textStream = result.textStream;
  58. const stream = new ReadableStream({
  59. async start(controller) {
  60. for await (const chunk of textStream) {
  61. controller.enqueue(encoder.encode(chunk));
  62. }
  63. // Append usage metadata after stream completes
  64. try {
  65. const usage = await result.usage;
  66. const meta = JSON.stringify({
  67. __meta: "usage",
  68. promptTokens: usage.inputTokens,
  69. completionTokens: usage.outputTokens,
  70. totalTokens: usage.totalTokens,
  71. });
  72. controller.enqueue(encoder.encode(`\n${meta}\n`));
  73. } catch {
  74. // Usage not available -- skip silently
  75. }
  76. controller.close();
  77. },
  78. });
  79. return new Response(stream, {
  80. headers: { "Content-Type": "text/plain; charset=utf-8" },
  81. });
  82. }