diff --git a/Dockerfile b/Dockerfile index cbe0188aaee92186937765d2c85d76f7b212c537..951ef86411380a727577232527792d5178e49773 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,12 @@ -FROM node:20-alpine -USER root +FROM node:22 USER 1000 WORKDIR /usr/src/app -# Copy package.json and package-lock.json to the container -COPY --chown=1000 package.json package-lock.json ./ # Copy the rest of the application files to the container COPY --chown=1000 . . -RUN npm install -RUN npm run build +RUN npm install && npm run build # Expose the application port (assuming your app runs on port 3000) EXPOSE 3000 diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts index 5be66c0cfd57a6d19839eae74aff94db60ff2a2c..b095fc9fe53ffbd2b3ab2047375855db2db679e4 100644 --- a/app/api/ask/route.ts +++ b/app/api/ask/route.ts @@ -285,11 +285,35 @@ export async function PUT(request: NextRequest) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; + const normalizeHtml = (html: string): string => { + return html + // Normalize whitespace within tags + .replace(/\s+/g, ' ') + // Remove spaces before closing > + .replace(/\s+>/g, '>') + // Remove spaces before /> + .replace(/\s+\/>/g, '/>') + // Normalize spaces around = in attributes + .replace(/\s*=\s*/g, '=') + // Normalize quotes (convert single to double) + .replace(/='([^']*)'/g, '="$1"') + // Remove trailing spaces in opening/closing tags + .replace(/<([^>]*?)\s+>/g, '<$1>') + // Normalize self-closing tags + .replace(/\/\s*>/g, '/>') + .trim(); + }; + const createFlexibleHtmlRegex = (searchBlock: string) => { - let searchRegex = escapeRegExp(searchBlock) - .replace(/\s+/g, '\\s*') + // Normalize both the search block for comparison + const normalizedSearch = normalizeHtml(searchBlock); + + // Escape regex special characters + let searchRegex = escapeRegExp(normalizedSearch) + // Make whitespace flexible (but only between elements, not within tags) .replace(/>\s*\\s*<') - .replace(/\s*>/g, '\\s*>'); + // Make line breaks and spaces around content flexible + .replace(/>\s*([^<]+)\s*\\s*$1\\s*<'); return new RegExp(searchRegex, 'g'); }; @@ -450,17 +474,49 @@ export async function PUT(request: NextRequest) { updatedLines.push([1, replaceBlock.split("\n").length]); } else { const regex = createFlexibleHtmlRegex(searchBlock); - const match = regex.exec(pageHtml); + + // Normalize the pageHtml for matching + const normalizedPageHtml = normalizeHtml(pageHtml); + const match = regex.exec(normalizedPageHtml); 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); + // Find the original match in the non-normalized HTML + const normalizedSearch = normalizeHtml(searchBlock); + const originalMatchIndex = pageHtml.indexOf(searchBlock); + + if (originalMatchIndex !== -1) { + const beforeText = pageHtml.substring(0, originalMatchIndex); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + pageHtml = pageHtml.replace(searchBlock, replaceBlock); + } else { + // Fallback: try to find similar pattern in the original HTML + const flexibleRegex = new RegExp( + escapeRegExp(searchBlock) + .replace(/\s+/g, '\\s+') + .replace(/\s*=\s*/g, '\\s*=\\s*') + .replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'") + .replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"') + .replace(/\s*>/g, '\\s*>') + .replace(/\s*\/>/g, '\\s*/>'), + 'g' + ); + + const flexibleMatch = flexibleRegex.exec(pageHtml); + if (flexibleMatch) { + const matchedText = flexibleMatch[0]; + const beforeText = pageHtml.substring(0, flexibleMatch.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); + } + } } } @@ -539,17 +595,54 @@ export async function PUT(request: NextRequest) { updatedLines.push([1, replaceBlock.split("\n").length]); } else { const regex = createFlexibleHtmlRegex(searchBlock); - const match = regex.exec(newHtml); + + // Get the main page HTML (first page or index page) + const mainPage = updatedPages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index') || updatedPages[0]; + if (!mainPage) continue; + + newHtml = mainPage.html; + + // Normalize the newHtml for matching + const normalizedNewHtml = normalizeHtml(newHtml); + const match = regex.exec(normalizedNewHtml); 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); + // Find the original match in the non-normalized HTML + const originalMatchIndex = newHtml.indexOf(searchBlock); + + if (originalMatchIndex !== -1) { + const beforeText = newHtml.substring(0, originalMatchIndex); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + newHtml = newHtml.replace(searchBlock, replaceBlock); + } else { + // Fallback: try to find similar pattern in the original HTML + const flexibleRegex = new RegExp( + escapeRegExp(searchBlock) + .replace(/\s+/g, '\\s+') + .replace(/\s*=\s*/g, '\\s*=\\s*') + .replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'") + .replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"') + .replace(/\s*>/g, '\\s*>') + .replace(/\s*\/>/g, '\\s*/>'), + 'g' + ); + + const flexibleMatch = flexibleRegex.exec(newHtml); + if (flexibleMatch) { + const matchedText = flexibleMatch[0]; + const beforeText = newHtml.substring(0, flexibleMatch.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); + } + } } } diff --git a/hooks/useAi.ts b/hooks/useAi.ts index 01cbde2e088540b19510c6cae6f4806c5de2c7f7..60a1702dff1f5bf2bf6f10534c28ff01f85831e3 100644 --- a/hooks/useAi.ts +++ b/hooks/useAi.ts @@ -10,7 +10,6 @@ import { api } from "@/lib/api"; import { useRouter } from "next/navigation"; import { useUser } from "./useUser"; import { LivePreviewRef } from "@/components/editor/live-preview"; -import { isTheSameHtml } from "@/lib/compare-html-diff"; export const useAi = (onScrollToBottom?: () => void, livePreviewRef?: React.RefObject) => { const client = useQueryClient(); @@ -199,15 +198,10 @@ export const useAi = (onScrollToBottom?: () => void, livePreviewRef?: React.RefO } const newPages = formatPages(contentResponse); - let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim(); - if (!projectName) { - projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40); - } + const projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim(); setPages(newPages); - setLastSavedPages([...newPages]); - if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) { - createNewProject(prompt, newPages, projectName, isLoggedIn); - } + setLastSavedPages([...newPages]); // Mark initial pages as saved + createNewProject(prompt, newPages, projectName, isLoggedIn); setPrompts([...prompts, prompt]); return { success: true, pages: newPages };