omnidev / app /api /ask /route.ts
kalhdrawi's picture
Reupload OmniDev clean version
48357fd
raw
history blame
22.8 kB
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { InferenceClient } from "@huggingface/inference";
import { GoogleGenAI } from "@google/genai";
import { MODELS } from "@/lib/providers";
import {
DIVIDER,
FOLLOW_UP_SYSTEM_PROMPT,
INITIAL_SYSTEM_PROMPT,
MAX_REQUESTS_PER_IP,
NEW_PAGE_END,
NEW_PAGE_START,
REPLACE_END,
SEARCH_START,
UPDATE_PAGE_START,
UPDATE_PAGE_END,
PROMPT_FOR_PROJECT_NAME,
} from "@/lib/prompts";
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
import { Page } from "@/types";
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
import { isAuthenticated } from "@/lib/auth";
import { getBestProvider } from "@/lib/best-provider";
// import { rewritePrompt } from "@/lib/rewrite-prompt";
import { COLORS } from "@/lib/utils";
import { templates } from "@/lib/templates";
const ipAddresses = new Map();
const IS_UNLIMITED_MODE = (process.env.UNLIMITED_MODE || '').toLowerCase() === 'true';
export async function POST(request: NextRequest) {
const authHeaders = await headers();
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
const body = await request.json();
const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
if (!model || (!prompt && !redesignMarkdown)) {
return NextResponse.json(
{ ok: false, error: "Missing required fields" },
{ status: 400 }
);
}
const selectedModel = MODELS.find(
(m) => m.value === model || m.label === model
);
if (!selectedModel) {
return NextResponse.json(
{ ok: false, error: "Invalid model selected" },
{ status: 400 }
);
}
let token: string | null = null;
if (userToken) token = userToken;
let billTo: string | null = null;
/**
* Handle local usage token, this bypass the need for a user token
* and allows local testing without authentication.
* This is useful for development and testing purposes.
*/
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
token = process.env.HF_TOKEN;
}
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
: authHeaders.get("x-forwarded-for");
if (!token) {
if (!IS_UNLIMITED_MODE) {
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
}
if (!IS_UNLIMITED_MODE && ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
return NextResponse.json(
{
ok: false,
openLogin: true,
message: "Log In to continue using the service",
},
{ status: 429 }
);
}
token = process.env.DEFAULT_HF_TOKEN as string;
billTo = "huggingface";
}
const selectedProvider = await getBestProvider(selectedModel.value, provider)
let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt;
if (enhancedSettings.isActive) {
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
}
try {
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const response = new NextResponse(stream.readable, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
(async () => {
// let completeResponse = "";
try {
const systemPrompt = INITIAL_SYSTEM_PROMPT;
const userPrompt = rewrittenPrompt + (enhancedSettings.isActive ? `\n\n1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).\n2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).\n3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "");
// Special handling for Google Gemini
if (selectedProvider.provider === "google") {
if (!process.env.GEMINI_API_KEY) {
throw new Error("Missing GEMINI_API_KEY in environment");
}
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const contents = [
{ role: "user", parts: [{ text: userPrompt }] },
];
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
const config: any = {
maxOutputTokens: dynamicMaxTokens,
};
const response = await ai.models.generateContentStream({
model: selectedModel.value,
config,
contents: [
{ role: "user", parts: [{ text: systemPrompt }] },
...contents,
],
});
// Newer SDKs return an async iterable of chunks with `.text`
// Some versions expose `.stream` iterable; support both.
const iterable: any = (response as any)?.stream ?? response;
for await (const chunk of iterable as AsyncIterable<any>) {
const text = (chunk && (chunk.text ?? chunk?.candidates?.[0]?.content?.parts?.[0]?.text)) || "";
if (text) {
await writer.write(encoder.encode(text));
}
}
} else {
// Default path via Hugging Face Inference Router
const client = new InferenceClient(token);
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
const chatCompletion = client.chatCompletionStream(
{
model: selectedModel.value,
provider: selectedProvider.provider,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
...providerConfig,
},
billTo ? { billTo } : {}
);
while (true) {
const { done, value } = await chatCompletion.next()
if (done) break;
const chunk = value.choices[0]?.delta?.content;
if (chunk) await writer.write(encoder.encode(chunk));
}
}
// Explicitly close the writer after successful completion
await writer.close();
} catch (error: any) {
if (!IS_UNLIMITED_MODE && error.message?.includes("exceeded your monthly included credits")) {
await writer.write(
encoder.encode(
JSON.stringify({
ok: false,
openProModal: true,
message: error.message,
})
)
);
} else if (error?.message?.includes("inference provider information")) {
await writer.write(
encoder.encode(
JSON.stringify({
ok: false,
openSelectProvider: true,
message: error.message,
})
)
);
}
else {
await writer.write(
encoder.encode(
JSON.stringify({
ok: false,
message:
error.message ||
"An error occurred while processing your request.",
})
)
);
}
} finally {
// Ensure the writer is always closed, even if already closed
try {
await writer?.close();
} catch {
// Ignore errors when closing the writer as it might already be closed
}
}
})();
return response;
} catch (error: any) {
return NextResponse.json(
{
ok: false,
openSelectProvider: true,
message:
error?.message || "An error occurred while processing your request.",
},
{ status: 500 }
);
}
}
export async function PUT(request: NextRequest) {
console.log("PUT request received");
const user = await isAuthenticated();
if (user instanceof NextResponse || !user) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const authHeaders = await headers();
const body = await request.json();
const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
body;
let repoId = repoIdFromBody;
if (!prompt || pages.length === 0) {
return NextResponse.json(
{ ok: false, error: "Missing required fields" },
{ status: 400 }
);
}
const selectedModel = MODELS.find(
(m) => m.value === model || m.label === model
);
if (!selectedModel) {
return NextResponse.json(
{ ok: false, error: "Invalid model selected" },
{ status: 400 }
);
}
let token = user.token as string;
let billTo: string | null = null;
/**
* Handle local usage token, this bypass the need for a user token
* and allows local testing without authentication.
* This is useful for development and testing purposes.
*/
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
token = process.env.HF_TOKEN;
}
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
: authHeaders.get("x-forwarded-for");
if (!token) {
if (!IS_UNLIMITED_MODE) {
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
}
if (!IS_UNLIMITED_MODE && ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
return NextResponse.json(
{
ok: false,
openLogin: true,
message: "Log In to continue using the service",
},
{ status: 429 }
);
}
token = process.env.DEFAULT_HF_TOKEN as string;
billTo = "huggingface";
}
const client = new InferenceClient(token);
const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
const createFlexibleHtmlRegex = (searchBlock: string) => {
let searchRegex = escapeRegExp(searchBlock)
.replace(/\s+/g, '\\s*')
.replace(/>\s*</g, '>\\s*<')
.replace(/\s*>/g, '\\s*>');
return new RegExp(searchRegex, 'g');
};
const selectedProvider = await getBestProvider(selectedModel.value, provider)
try {
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
const userContext = "You are modifying the HTML file based on the user's request.";
// Send all pages without filtering
const allPages = pages || [];
const pagesContext = allPages
.map((p: Page) => `- ${p.path}\n${p.html}`)
.join("\n\n");
const assistantContext = `${
selectedElementHtml
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
: ""
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
let chunk = "";
if (selectedProvider.provider === "google") {
if (!process.env.GEMINI_API_KEY) {
return NextResponse.json({ ok: false, message: "Missing GEMINI_API_KEY in environment" }, { status: 500 });
}
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const contents = [
{ role: "user", parts: [{ text: userContext }] },
{ role: "model", parts: [{ text: assistantContext }] },
{ role: "user", parts: [{ text: prompt }] },
];
const config: any = { maxOutputTokens: dynamicMaxTokens };
const response = await ai.models.generateContentStream({
model: selectedModel.value,
config,
contents: [
{ role: "user", parts: [{ text: systemPrompt }] },
...contents,
],
});
const iterable: any = (response as any)?.stream ?? response;
for await (const c of iterable as AsyncIterable<any>) {
const text = (c && (c.text ?? c?.candidates?.[0]?.content?.parts?.[0]?.text)) || "";
if (text) chunk += text;
}
} else {
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
const chatCompletion = client.chatCompletionStream(
{
model: selectedModel.value,
provider: selectedProvider.provider,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContext },
{ role: "assistant", content: assistantContext },
{ role: "user", content: prompt },
],
...providerConfig,
},
billTo ? { billTo } : {}
);
while (true) {
const { done, value } = await chatCompletion.next();
if (done) break;
const deltaContent = value.choices[0]?.delta?.content;
if (deltaContent) chunk += deltaContent;
}
}
if (!chunk) {
return NextResponse.json(
{ ok: false, message: "No content returned from the model" },
{ status: 400 }
);
}
if (chunk) {
const updatedLines: number[][] = [];
let newHtml = "";
const updatedPages = [...(pages || [])];
const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
let updatePageMatch;
while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
const [, pagePath, pageContent] = updatePageMatch;
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
if (pageIndex !== -1) {
let pageHtml = updatedPages[pageIndex].html;
let processedContent = pageContent;
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
if (htmlMatch) {
processedContent = htmlMatch[1];
}
let position = 0;
let moreBlocks = true;
while (moreBlocks) {
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
if (searchStartIndex === -1) {
moreBlocks = false;
continue;
}
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
if (dividerIndex === -1) {
moreBlocks = false;
continue;
}
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
if (replaceEndIndex === -1) {
moreBlocks = false;
continue;
}
const searchBlock = processedContent.substring(
searchStartIndex + SEARCH_START.length,
dividerIndex
);
const replaceBlock = processedContent.substring(
dividerIndex + DIVIDER.length,
replaceEndIndex
);
if (searchBlock.trim() === "") {
pageHtml = `${replaceBlock}\n${pageHtml}`;
updatedLines.push([1, replaceBlock.split("\n").length]);
} else {
const regex = createFlexibleHtmlRegex(searchBlock);
const match = regex.exec(pageHtml);
if (match) {
const matchedText = match[0];
const beforeText = pageHtml.substring(0, match.index);
const startLineNumber = beforeText.split("\n").length;
const replaceLines = replaceBlock.split("\n").length;
const endLineNumber = startLineNumber + replaceLines - 1;
updatedLines.push([startLineNumber, endLineNumber]);
pageHtml = pageHtml.replace(matchedText, replaceBlock);
}
}
position = replaceEndIndex + REPLACE_END.length;
}
updatedPages[pageIndex].html = pageHtml;
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
newHtml = pageHtml;
}
}
}
const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
let newPageMatch;
while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
const [, pagePath, pageContent] = newPageMatch;
let pageHtml = pageContent;
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
if (htmlMatch) {
pageHtml = htmlMatch[1];
}
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
if (existingPageIndex !== -1) {
updatedPages[existingPageIndex] = {
path: pagePath,
html: pageHtml.trim()
};
} else {
updatedPages.push({
path: pagePath,
html: pageHtml.trim()
});
}
}
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
let position = 0;
let moreBlocks = true;
while (moreBlocks) {
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
if (searchStartIndex === -1) {
moreBlocks = false;
continue;
}
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
if (dividerIndex === -1) {
moreBlocks = false;
continue;
}
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
if (replaceEndIndex === -1) {
moreBlocks = false;
continue;
}
const searchBlock = chunk.substring(
searchStartIndex + SEARCH_START.length,
dividerIndex
);
const replaceBlock = chunk.substring(
dividerIndex + DIVIDER.length,
replaceEndIndex
);
if (searchBlock.trim() === "") {
newHtml = `${replaceBlock}\n${newHtml}`;
updatedLines.push([1, replaceBlock.split("\n").length]);
} else {
const regex = createFlexibleHtmlRegex(searchBlock);
const match = regex.exec(newHtml);
if (match) {
const matchedText = match[0];
const beforeText = newHtml.substring(0, match.index);
const startLineNumber = beforeText.split("\n").length;
const replaceLines = replaceBlock.split("\n").length;
const endLineNumber = startLineNumber + replaceLines - 1;
updatedLines.push([startLineNumber, endLineNumber]);
newHtml = newHtml.replace(matchedText, replaceBlock);
}
}
position = replaceEndIndex + REPLACE_END.length;
}
// Update the main HTML if it's the index page
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
if (mainPageIndex !== -1) {
updatedPages[mainPageIndex].html = newHtml;
}
}
const files: File[] = [];
updatedPages.forEach((page: Page) => {
const file = new File([page.html], page.path, { type: "text/html" });
files.push(file);
});
if (isNew) {
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
const formattedTitle = projectName?.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.split("-")
.filter(Boolean)
.join("-")
.slice(0, 96);
const repo: RepoDesignation = {
type: "space",
name: `${user.name}/${formattedTitle}`,
};
const { repoUrl} = await createRepo({
repo,
accessToken: user.token as string,
});
repoId = repoUrl.split("/").slice(-2).join("/");
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
const README = `---
title: ${projectName}
emoji: 🚀
colorFrom: ${colorFrom}
colorTo: ${colorTo}
sdk: docker
app_port: 7860
pinned: false
---
# ${projectName}
This Space was scaffolded by OmniDev (Full‑Stack ready).
`;
files.push(new File([README], "README.md", { type: "text/markdown" }));
}
const response = await uploadFiles({
repo: {
type: "space",
name: repoId,
},
files,
commitTitle: prompt,
accessToken: user.token as string,
});
return NextResponse.json({
ok: true,
updatedLines,
pages: updatedPages,
repoId,
commit: {
...response.commit,
title: prompt,
}
});
} else {
return NextResponse.json(
{ ok: false, message: "No content returned from the model" },
{ status: 400 }
);
}
} catch (error: any) {
if (!IS_UNLIMITED_MODE && error.message?.includes("exceeded your monthly included credits")) {
return NextResponse.json(
{
ok: false,
openProModal: true,
message: error.message,
},
{ status: 402 }
);
}
return NextResponse.json(
{
ok: false,
openSelectProvider: true,
message:
error.message || "An error occurred while processing your request.",
},
{ status: 500 }
);
}
}