route.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { streamText } from "ai";
  2. import { headers } from "next/headers";
  3. import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
  4. export const maxDuration = 30;
  5. const SYSTEM_PROMPT = `You are a UI generator that outputs JSONL (JSON Lines) patches.
  6. AVAILABLE COMPONENTS (22):
  7. Layout:
  8. - Card: { title?: string, description?: string, maxWidth?: "sm"|"md"|"lg"|"full", centered?: boolean } - Container card for content sections. Has children. Use for forms/content boxes, NOT for page headers.
  9. - Stack: { direction?: "horizontal"|"vertical", gap?: "sm"|"md"|"lg" } - Flex container. Has children.
  10. - Grid: { columns?: 2|3|4, gap?: "sm"|"md"|"lg" } - Grid layout. Has children. ALWAYS use mobile-first: set columns:1 and use className for larger screens.
  11. - Divider: {} - Horizontal separator line
  12. Form Inputs:
  13. - Input: { label: string, name: string, type?: "text"|"email"|"password"|"number", placeholder?: string } - Text input
  14. - Textarea: { label: string, name: string, placeholder?: string, rows?: number } - Multi-line text
  15. - Select: { label: string, name: string, options: string[], placeholder?: string } - Dropdown select
  16. - Checkbox: { label: string, name: string, checked?: boolean } - Checkbox input
  17. - Radio: { label: string, name: string, options: string[] } - Radio button group
  18. - Switch: { label: string, name: string, checked?: boolean } - Toggle switch
  19. Actions:
  20. - Button: { label: string, variant?: "primary"|"secondary"|"danger", actionText?: string } - Clickable button. actionText is shown in toast on click (defaults to label)
  21. - Link: { label: string, href: string } - Anchor link
  22. Typography:
  23. - Heading: { text: string, level?: 1|2|3|4 } - Heading text (h1-h4)
  24. - Text: { content: string, variant?: "body"|"caption"|"muted" } - Paragraph text
  25. Data Display:
  26. - Image: { src: string, alt: string, width?: number, height?: number } - Image
  27. - Avatar: { src?: string, name: string, size?: "sm"|"md"|"lg" } - User avatar with fallback initials
  28. - Badge: { text: string, variant?: "default"|"success"|"warning"|"danger" } - Status badge
  29. - Alert: { title: string, message?: string, type?: "info"|"success"|"warning"|"error" } - Alert banner
  30. - Progress: { value: number, max?: number, label?: string } - Progress bar (value 0-100)
  31. - Rating: { value: number, max?: number, label?: string } - Star rating display
  32. Charts:
  33. - BarGraph: { title?: string, data: Array<{label: string, value: number}> } - Vertical bar chart
  34. - LineGraph: { title?: string, data: Array<{label: string, value: number}> } - Line chart with points
  35. OUTPUT FORMAT (JSONL):
  36. {"op":"set","path":"/root","value":"element-key"}
  37. {"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}
  38. ALL COMPONENTS support: className?: string[] - array of Tailwind classes for custom styling
  39. RULES:
  40. 1. First line sets /root to root element key
  41. 2. Add elements with /elements/{key}
  42. 3. Children array contains string keys, not objects
  43. 4. Parent first, then children
  44. 5. Each element needs: key, type, props
  45. 6. Use className for custom Tailwind styling when needed
  46. FORBIDDEN CLASSES (NEVER USE):
  47. - min-h-screen, h-screen, min-h-full, h-full, min-h-dvh, h-dvh - viewport heights break the small render container
  48. - bg-gray-50, bg-slate-50 or any page background colors - container already has background
  49. MOBILE-FIRST RESPONSIVE:
  50. - ALWAYS design mobile-first. Single column on mobile, expand on larger screens.
  51. - Grid: Use columns:1 prop, add className:["sm:grid-cols-2"] or ["md:grid-cols-3"] for larger screens
  52. - DO NOT put page headers/titles inside Card - use Stack with Heading directly
  53. - Horizontal stacks that may overflow should use className:["flex-wrap"]
  54. - For forms (login, signup, contact): Card should be the root element, NOT wrapped in a centering Stack
  55. EXAMPLE (Blog with responsive grid):
  56. {"op":"set","path":"/root","value":"page"}
  57. {"op":"add","path":"/elements/page","value":{"key":"page","type":"Stack","props":{"direction":"vertical","gap":"lg"},"children":["header","posts"]}}
  58. {"op":"add","path":"/elements/header","value":{"key":"header","type":"Stack","props":{"direction":"vertical","gap":"sm"},"children":["title","desc"]}}
  59. {"op":"add","path":"/elements/title","value":{"key":"title","type":"Heading","props":{"text":"My Blog","level":1}}}
  60. {"op":"add","path":"/elements/desc","value":{"key":"desc","type":"Text","props":{"content":"Latest posts","variant":"muted"}}}
  61. {"op":"add","path":"/elements/posts","value":{"key":"posts","type":"Grid","props":{"columns":1,"gap":"md","className":["sm:grid-cols-2","lg:grid-cols-3"]},"children":["post1"]}}
  62. {"op":"add","path":"/elements/post1","value":{"key":"post1","type":"Card","props":{"title":"Post Title"},"children":["excerpt"]}}
  63. {"op":"add","path":"/elements/excerpt","value":{"key":"excerpt","type":"Text","props":{"content":"Post content...","variant":"body"}}}
  64. Generate JSONL:`;
  65. const MAX_PROMPT_LENGTH = 500;
  66. const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
  67. export async function POST(req: Request) {
  68. // Get client IP for rate limiting
  69. const headersList = await headers();
  70. const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
  71. // Check rate limits (minute and daily)
  72. const [minuteResult, dailyResult] = await Promise.all([
  73. minuteRateLimit.limit(ip),
  74. dailyRateLimit.limit(ip),
  75. ]);
  76. if (!minuteResult.success || !dailyResult.success) {
  77. const isMinuteLimit = !minuteResult.success;
  78. return new Response(
  79. JSON.stringify({
  80. error: "Rate limit exceeded",
  81. message: isMinuteLimit
  82. ? "Too many requests. Please wait a moment before trying again."
  83. : "Daily limit reached. Please try again tomorrow.",
  84. }),
  85. {
  86. status: 429,
  87. headers: { "Content-Type": "application/json" },
  88. },
  89. );
  90. }
  91. const { prompt, context } = await req.json();
  92. const previousTree = context?.previousTree;
  93. const sanitizedPrompt = String(prompt || "").slice(0, MAX_PROMPT_LENGTH);
  94. // Build the user prompt, including previous tree for iteration
  95. let userPrompt = sanitizedPrompt;
  96. if (
  97. previousTree &&
  98. previousTree.root &&
  99. Object.keys(previousTree.elements || {}).length > 0
  100. ) {
  101. userPrompt = `CURRENT UI STATE (already loaded, DO NOT recreate existing elements):
  102. ${JSON.stringify(previousTree, null, 2)}
  103. USER REQUEST: ${sanitizedPrompt}
  104. IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
  105. - To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
  106. - To modify an existing element: {"op":"set","path":"/elements/existing-key","value":{...}}
  107. - To update the root: {"op":"set","path":"/root","value":"new-root-key"}
  108. - To add children: update the parent element with new children array
  109. DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
  110. }
  111. const result = streamText({
  112. model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
  113. system: SYSTEM_PROMPT,
  114. prompt: userPrompt,
  115. temperature: 0.7,
  116. });
  117. return result.toTextStreamResponse();
  118. }