enzostvs/deepsite
17k
1import {2 END_FILE_CONTENT,3 END_PROJECT_NAME,4 START_FILE_CONTENT,5 START_PROJECT_NAME,6 SEARCH_START,7 DIVIDER,8 REPLACE_END,9} from "./prompts";10import { File } from "./type";11 12// todo: the Editing stuffs in message doesnt show when it"s a SEARCH and REPLACE operation, I mean it shows it but only at the end of the message, not during the generation. fix that.13 14/**15 * Validates that a filename has an extension.16 * Returns the filename if valid, null otherwise.17 * Allows dotfiles (like .gitignore) and regular files with extensions (like app.py).18 */19const validateFilename = (filename: string): string | null => {20 if (!filename) return null;21 22 const trimmed = filename.trim();23 if (!trimmed) return null;24 25 // Find the last dot in the filename26 const lastDotIndex = trimmed.lastIndexOf(".");27 28 // No dot found - invalid (no extension)29 if (lastDotIndex === -1) return null;30 31 // Dot at the end - invalid (no extension after dot)32 if (lastDotIndex === trimmed.length - 1) return null;33 34 // Dot at the start - this is a dotfile (like .gitignore), which is valid35 // Dot in the middle - this is a regular file with extension (like app.py), which is valid36 // Both cases are acceptable as long as there's something after the dot37 38 return trimmed;39};40 41/**42 * Check if the end of a string contains a partial match of a target string43 */44const hasPartialMarkerAtEnd = (text: string, marker: string): number => {45 // Check for partial matches starting from length 3 (to avoid false positives with common substrings)46 for (47 let len = Math.max(3, Math.floor(marker.length / 2));48 len < marker.length;49 len++50 ) {51 const partialMarker = marker.substring(0, len);52 if (text.endsWith(partialMarker)) {53 return len; // Return the length of the partial match54 }55 }56 return 0; // No partial match found57};58 59/**60 * Extract message content by removing all special markers and their content61 */62const extractMessageContent = (message: string): string => {63 let result = message;64 65 // Remove all complete START_FILE_CONTENT...END_FILE_CONTENT blocks66 const fileContentRegex = new RegExp(67 `${START_FILE_CONTENT}[\\s\\S]*?${END_FILE_CONTENT}`,68 "g"69 );70 result = result.replace(fileContentRegex, "");71 72 // Remove incomplete START_FILE_CONTENT blocks (streaming - no END_FILE_CONTENT yet)73 const incompleteFileIndex = result.indexOf(START_FILE_CONTENT);74 if (incompleteFileIndex !== -1) {75 result = result.substring(0, incompleteFileIndex);76 }77 78 // Remove all complete START_PROJECT_NAME...END_PROJECT_NAME blocks79 const projectNameRegex = new RegExp(80 `${START_PROJECT_NAME}[\\s\\S]*?${END_PROJECT_NAME}`,81 "g"82 );83 result = result.replace(projectNameRegex, "");84 85 // Remove incomplete START_PROJECT_NAME blocks (streaming - no END_PROJECT_NAME yet)86 const incompleteProjectIndex = result.indexOf(START_PROJECT_NAME);87 if (incompleteProjectIndex !== -1) {88 result = result.substring(0, incompleteProjectIndex);89 }90 91 // Remove all complete SEARCH_START...REPLACE_END blocks92 const searchReplaceRegex = new RegExp(93 `${SEARCH_START}[\\s\\S]*?${REPLACE_END}`,94 "g"95 );96 result = result.replace(searchReplaceRegex, "");97 98 // Remove incomplete SEARCH_START blocks (streaming - no REPLACE_END yet)99 const incompleteSearchIndex = result.indexOf(SEARCH_START);100 if (incompleteSearchIndex !== -1) {101 result = result.substring(0, incompleteSearchIndex);102 }103 104 // Handle incomplete/partial markers at the end (for streaming)105 // This is critical to prevent showing marker text to users during streaming106 const markers = [START_FILE_CONTENT, START_PROJECT_NAME, SEARCH_START];107 108 for (const marker of markers) {109 const partialLength = hasPartialMarkerAtEnd(result, marker);110 if (partialLength > 0) {111 result = result.substring(0, result.length - partialLength);112 break; // Only one marker can be at the end113 }114 }115 116 // Clean up extra whitespace117 return result.trim();118};119 120export const formatResponse = (message: string, currentFiles: File[]) => {121 // Track which files have been created or modified122 const modifiedFiles = new Map<string, File>();123 124 // Extract message content (everything outside special markers)125 const messageContent = extractMessageContent(message);126 127 // Extract project title128 let projectTitle =129 message.split(START_PROJECT_NAME)[1]?.split(END_PROJECT_NAME)[0]?.trim() ??130 "";131 132 // Handle partial project title markers133 if (projectTitle && !message.includes(END_PROJECT_NAME)) {134 for (let i = END_PROJECT_NAME.length - 1; i > 0; i--) {135 const partialMarker = END_PROJECT_NAME.substring(0, i);136 if (projectTitle.endsWith(partialMarker)) {137 projectTitle = projectTitle138 .substring(0, projectTitle.length - partialMarker.length)139 .trim();140 break;141 }142 }143 }144 145 if (message.includes(SEARCH_START)) {146 const searchSections = message.split(SEARCH_START).slice(1);147 148 for (149 let sectionIndex = 0;150 sectionIndex < searchSections.length;151 sectionIndex++152 ) {153 const section = searchSections[sectionIndex];154 const isLastSection = sectionIndex === searchSections.length - 1;155 156 // Check if this section has a complete REPLACE_END marker157 const hasReplaceEnd = section.includes(REPLACE_END);158 159 // For incomplete sections (last section without REPLACE_END), skip processing160 // to avoid applying partial replacements that corrupt the file161 if (isLastSection && !hasReplaceEnd) {162 continue;163 }164 165 const searchPart = section.split(REPLACE_END)[0];166 if (!searchPart) continue;167 168 const dividerIndex = searchPart.indexOf(DIVIDER);169 if (dividerIndex === -1) continue;170 171 const searchContent = searchPart.substring(0, dividerIndex);172 const replaceContent = searchPart.substring(173 dividerIndex + DIVIDER.length174 );175 176 if (!searchContent || !replaceContent) continue;177 178 // Extract filename from the first line (same line as SEARCH_START)179 const firstNewlineIndex = searchContent.indexOf("\n");180 let filename = "";181 let searchContentAfterFilename = searchContent;182 183 if (firstNewlineIndex !== -1) {184 // Filename is on the first line (same line as SEARCH_START marker)185 filename = searchContent.substring(0, firstNewlineIndex).trim();186 searchContentAfterFilename = searchContent.substring(187 firstNewlineIndex + 1188 );189 } else {190 // No newline found, entire content might be just the filename191 filename = searchContent.trim();192 searchContentAfterFilename = "";193 }194 195 // Validate filename has extension196 const validatedFilename = validateFilename(filename);197 if (!validatedFilename) continue;198 filename = validatedFilename;199 200 const searchCodeStart = searchContentAfterFilename.indexOf("```");201 if (searchCodeStart === -1) continue;202 203 const searchCodeBlock =204 searchContentAfterFilename.substring(searchCodeStart);205 let searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);206 if (!searchMatch) {207 searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);208 }209 if (!searchMatch) continue;210 211 const searchText = searchMatch[1].trim();212 213 const replaceCodeStart = replaceContent.indexOf("```");214 if (replaceCodeStart === -1) continue;215 216 const replaceCodeBlock = replaceContent.substring(replaceCodeStart);217 let replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);218 if (!replaceMatch) {219 replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);220 }221 if (!replaceMatch) continue;222 223 let replaceText = replaceMatch[1];224 for (let i = 1; i < 3; i++) {225 const partialClosing = "`".repeat(i);226 if (replaceText.endsWith(partialClosing)) {227 replaceText = replaceText.substring(0, replaceText.length - i);228 break;229 }230 }231 replaceText = replaceText.trim();232 233 // First check if we already have a modified version of this file in the current operation234 const existingModifiedFile = modifiedFiles.get(filename);235 const fileToModify =236 existingModifiedFile || currentFiles.find((f) => f.path === filename);237 238 if (fileToModify && fileToModify.content) {239 const newContent = fileToModify.content.replace(240 searchText,241 replaceText242 );243 if (newContent !== fileToModify.content) {244 modifiedFiles.set(filename, { path: filename, content: newContent });245 }246 } else if (!fileToModify) {247 modifiedFiles.set(filename, { path: filename, content: replaceText });248 }249 }250 }251 252 // Handle new file creation blocks253 if (message.includes(START_FILE_CONTENT)) {254 const fileSections = message.split(START_FILE_CONTENT).slice(1);255 256 for (const section of fileSections) {257 const rawContent = section.split(END_FILE_CONTENT)[0] || "";258 259 // Extract filename - it's on the same line as START_FILE_CONTENT (before first newline)260 const firstNewlineIndex = rawContent.indexOf("\n");261 let path = "";262 let contentAfterFilename = rawContent;263 264 if (firstNewlineIndex !== -1) {265 // Filename is before the first newline (same line as START_FILE_CONTENT marker)266 path = rawContent.substring(0, firstNewlineIndex).trim();267 contentAfterFilename = rawContent.substring(firstNewlineIndex + 1);268 } else {269 // No newline found, entire content might be just the filename270 path = rawContent.trim();271 contentAfterFilename = "";272 }273 274 // Validate that path has an extension275 const validatedPath = validateFilename(path);276 if (!validatedPath) continue;277 path = validatedPath;278 279 // Find code block280 const codeBlockStart = contentAfterFilename.indexOf("```");281 282 if (codeBlockStart === -1) {283 // No code block, use raw content after filename284 const content = contentAfterFilename.trim();285 if (content || !currentFiles.find((f) => f.path === path)) {286 // Always add files from the message287 modifiedFiles.set(path, { path, content });288 }289 } else {290 // Has code block - extract content between ``` markers291 const afterCodeBlockStart =292 contentAfterFilename.substring(codeBlockStart);293 294 // Try to match complete code block first295 const completeMatch = afterCodeBlockStart.match(296 /^```[\w]*\n?([\s\S]*?)```/297 );298 299 if (completeMatch) {300 const content = completeMatch[1].trim();301 modifiedFiles.set(path, { path, content });302 } else {303 // Incomplete code block (streaming) - match everything after opening ```304 const incompleteMatch = afterCodeBlockStart.match(305 /^```[\w]*\n?([\s\S]*)/306 );307 if (incompleteMatch) {308 let content = incompleteMatch[1];309 // Remove trailing partial closing marker if present310 for (let i = 1; i < 3; i++) {311 const partialClosing = "`".repeat(i);312 if (content.endsWith(partialClosing)) {313 content = content.substring(0, content.length - i);314 break;315 }316 }317 content = content.trim();318 modifiedFiles.set(path, { path, content });319 }320 }321 }322 }323 }324 325 // Convert modified files map to array326 const files = Array.from(modifiedFiles.values())?.filter(327 (file) => file.path !== "README.md"328 );329 330 return {331 messageContent,332 files,333 projectTitle,334 };335};336 