route.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  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. 'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
  16. ],
  17. });
  18. const MAX_PROMPT_LENGTH = 500;
  19. const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
  20. export async function POST(req: Request) {
  21. // Get client IP for rate limiting
  22. const headersList = await headers();
  23. const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
  24. // Check rate limits (minute and daily)
  25. const [minuteResult, dailyResult] = await Promise.all([
  26. minuteRateLimit.limit(ip),
  27. dailyRateLimit.limit(ip),
  28. ]);
  29. if (!minuteResult.success || !dailyResult.success) {
  30. const isMinuteLimit = !minuteResult.success;
  31. return new Response(
  32. JSON.stringify({
  33. error: "Rate limit exceeded",
  34. message: isMinuteLimit
  35. ? "Too many requests. Please wait a moment before trying again."
  36. : "Daily limit reached. Please try again tomorrow.",
  37. }),
  38. {
  39. status: 429,
  40. headers: { "Content-Type": "application/json" },
  41. },
  42. );
  43. }
  44. const { prompt, context } = await req.json();
  45. const userPrompt = buildUserPrompt({
  46. prompt,
  47. currentSpec: context?.previousSpec,
  48. maxPromptLength: MAX_PROMPT_LENGTH,
  49. });
  50. const result = streamText({
  51. model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
  52. system: SYSTEM_PROMPT,
  53. prompt: userPrompt,
  54. temperature: 0.7,
  55. });
  56. // Stream the text, then append token usage metadata at the end
  57. const encoder = new TextEncoder();
  58. const textStream = result.textStream;
  59. const stream = new ReadableStream({
  60. async start(controller) {
  61. for await (const chunk of textStream) {
  62. controller.enqueue(encoder.encode(chunk));
  63. }
  64. // Append usage metadata after stream completes
  65. try {
  66. const usage = await result.usage;
  67. const meta = JSON.stringify({
  68. __meta: "usage",
  69. promptTokens: usage.inputTokens,
  70. completionTokens: usage.outputTokens,
  71. totalTokens: usage.totalTokens,
  72. });
  73. controller.enqueue(encoder.encode(`\n${meta}\n`));
  74. } catch {
  75. // Usage not available — skip silently
  76. }
  77. controller.close();
  78. },
  79. });
  80. return new Response(stream, {
  81. headers: { "Content-Type": "text/plain; charset=utf-8" },
  82. });
  83. }