Spaces:
Running
Running
| import { | |
| END_FILE_CONTENT, | |
| END_PROJECT_NAME, | |
| START_FILE_CONTENT, | |
| START_PROJECT_NAME, | |
| SEARCH_START, | |
| DIVIDER, | |
| REPLACE_END, | |
| } from "./prompts"; | |
| import { File } from "./type"; | |
| // 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. | |
| /** | |
| * Validates that a filename has an extension. | |
| * Returns the filename if valid, null otherwise. | |
| * Allows dotfiles (like .gitignore) and regular files with extensions (like app.py). | |
| */ | |
| const validateFilename = (filename: string): string | null => { | |
| if (!filename) return null; | |
| const trimmed = filename.trim(); | |
| if (!trimmed) return null; | |
| // Find the last dot in the filename | |
| const lastDotIndex = trimmed.lastIndexOf("."); | |
| // No dot found - invalid (no extension) | |
| if (lastDotIndex === -1) return null; | |
| // Dot at the end - invalid (no extension after dot) | |
| if (lastDotIndex === trimmed.length - 1) return null; | |
| // Dot at the start - this is a dotfile (like .gitignore), which is valid | |
| // Dot in the middle - this is a regular file with extension (like app.py), which is valid | |
| // Both cases are acceptable as long as there's something after the dot | |
| return trimmed; | |
| }; | |
| /** | |
| * Check if the end of a string contains a partial match of a target string | |
| */ | |
| const hasPartialMarkerAtEnd = (text: string, marker: string): number => { | |
| // Check for partial matches starting from length 3 (to avoid false positives with common substrings) | |
| for ( | |
| let len = Math.max(3, Math.floor(marker.length / 2)); | |
| len < marker.length; | |
| len++ | |
| ) { | |
| const partialMarker = marker.substring(0, len); | |
| if (text.endsWith(partialMarker)) { | |
| return len; // Return the length of the partial match | |
| } | |
| } | |
| return 0; // No partial match found | |
| }; | |
| /** | |
| * Extract message content by removing all special markers and their content | |
| */ | |
| const extractMessageContent = (message: string): string => { | |
| let result = message; | |
| // Remove all complete START_FILE_CONTENT...END_FILE_CONTENT blocks | |
| const fileContentRegex = new RegExp( | |
| `${START_FILE_CONTENT}[\\s\\S]*?${END_FILE_CONTENT}`, | |
| "g" | |
| ); | |
| result = result.replace(fileContentRegex, ""); | |
| // Remove incomplete START_FILE_CONTENT blocks (streaming - no END_FILE_CONTENT yet) | |
| const incompleteFileIndex = result.indexOf(START_FILE_CONTENT); | |
| if (incompleteFileIndex !== -1) { | |
| result = result.substring(0, incompleteFileIndex); | |
| } | |
| // Remove all complete START_PROJECT_NAME...END_PROJECT_NAME blocks | |
| const projectNameRegex = new RegExp( | |
| `${START_PROJECT_NAME}[\\s\\S]*?${END_PROJECT_NAME}`, | |
| "g" | |
| ); | |
| result = result.replace(projectNameRegex, ""); | |
| // Remove incomplete START_PROJECT_NAME blocks (streaming - no END_PROJECT_NAME yet) | |
| const incompleteProjectIndex = result.indexOf(START_PROJECT_NAME); | |
| if (incompleteProjectIndex !== -1) { | |
| result = result.substring(0, incompleteProjectIndex); | |
| } | |
| // Remove all complete SEARCH_START...REPLACE_END blocks | |
| const searchReplaceRegex = new RegExp( | |
| `${SEARCH_START}[\\s\\S]*?${REPLACE_END}`, | |
| "g" | |
| ); | |
| result = result.replace(searchReplaceRegex, ""); | |
| // Remove incomplete SEARCH_START blocks (streaming - no REPLACE_END yet) | |
| const incompleteSearchIndex = result.indexOf(SEARCH_START); | |
| if (incompleteSearchIndex !== -1) { | |
| result = result.substring(0, incompleteSearchIndex); | |
| } | |
| // Handle incomplete/partial markers at the end (for streaming) | |
| // This is critical to prevent showing marker text to users during streaming | |
| const markers = [START_FILE_CONTENT, START_PROJECT_NAME, SEARCH_START]; | |
| for (const marker of markers) { | |
| const partialLength = hasPartialMarkerAtEnd(result, marker); | |
| if (partialLength > 0) { | |
| result = result.substring(0, result.length - partialLength); | |
| break; // Only one marker can be at the end | |
| } | |
| } | |
| // Clean up extra whitespace | |
| return result.trim(); | |
| }; | |
| export const formatResponse = (message: string, currentFiles: File[]) => { | |
| // Track which files have been created or modified | |
| const modifiedFiles = new Map<string, File>(); | |
| // Extract message content (everything outside special markers) | |
| const messageContent = extractMessageContent(message); | |
| // Extract project title | |
| let projectTitle = | |
| message.split(START_PROJECT_NAME)[1]?.split(END_PROJECT_NAME)[0]?.trim() ?? | |
| ""; | |
| // Handle partial project title markers | |
| if (projectTitle && !message.includes(END_PROJECT_NAME)) { | |
| for (let i = END_PROJECT_NAME.length - 1; i > 0; i--) { | |
| const partialMarker = END_PROJECT_NAME.substring(0, i); | |
| if (projectTitle.endsWith(partialMarker)) { | |
| projectTitle = projectTitle | |
| .substring(0, projectTitle.length - partialMarker.length) | |
| .trim(); | |
| break; | |
| } | |
| } | |
| } | |
| if (message.includes(SEARCH_START)) { | |
| const searchSections = message.split(SEARCH_START).slice(1); | |
| for ( | |
| let sectionIndex = 0; | |
| sectionIndex < searchSections.length; | |
| sectionIndex++ | |
| ) { | |
| const section = searchSections[sectionIndex]; | |
| const isLastSection = sectionIndex === searchSections.length - 1; | |
| // Check if this section has a complete REPLACE_END marker | |
| const hasReplaceEnd = section.includes(REPLACE_END); | |
| // For incomplete sections (last section without REPLACE_END), skip processing | |
| // to avoid applying partial replacements that corrupt the file | |
| if (isLastSection && !hasReplaceEnd) { | |
| continue; | |
| } | |
| const searchPart = section.split(REPLACE_END)[0]; | |
| if (!searchPart) continue; | |
| const dividerIndex = searchPart.indexOf(DIVIDER); | |
| if (dividerIndex === -1) continue; | |
| const searchContent = searchPart.substring(0, dividerIndex); | |
| const replaceContent = searchPart.substring( | |
| dividerIndex + DIVIDER.length | |
| ); | |
| if (!searchContent || !replaceContent) continue; | |
| // Extract filename from the first line (same line as SEARCH_START) | |
| const firstNewlineIndex = searchContent.indexOf("\n"); | |
| let filename = ""; | |
| let searchContentAfterFilename = searchContent; | |
| if (firstNewlineIndex !== -1) { | |
| // Filename is on the first line (same line as SEARCH_START marker) | |
| filename = searchContent.substring(0, firstNewlineIndex).trim(); | |
| searchContentAfterFilename = searchContent.substring( | |
| firstNewlineIndex + 1 | |
| ); | |
| } else { | |
| // No newline found, entire content might be just the filename | |
| filename = searchContent.trim(); | |
| searchContentAfterFilename = ""; | |
| } | |
| // Validate filename has extension | |
| const validatedFilename = validateFilename(filename); | |
| if (!validatedFilename) continue; | |
| filename = validatedFilename; | |
| const searchCodeStart = searchContentAfterFilename.indexOf("```"); | |
| if (searchCodeStart === -1) continue; | |
| const searchCodeBlock = | |
| searchContentAfterFilename.substring(searchCodeStart); | |
| let searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/); | |
| if (!searchMatch) { | |
| searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*)/); | |
| } | |
| if (!searchMatch) continue; | |
| const searchText = searchMatch[1].trim(); | |
| const replaceCodeStart = replaceContent.indexOf("```"); | |
| if (replaceCodeStart === -1) continue; | |
| const replaceCodeBlock = replaceContent.substring(replaceCodeStart); | |
| let replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/); | |
| if (!replaceMatch) { | |
| replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*)/); | |
| } | |
| if (!replaceMatch) continue; | |
| let replaceText = replaceMatch[1]; | |
| for (let i = 1; i < 3; i++) { | |
| const partialClosing = "`".repeat(i); | |
| if (replaceText.endsWith(partialClosing)) { | |
| replaceText = replaceText.substring(0, replaceText.length - i); | |
| break; | |
| } | |
| } | |
| replaceText = replaceText.trim(); | |
| // First check if we already have a modified version of this file in the current operation | |
| const existingModifiedFile = modifiedFiles.get(filename); | |
| const fileToModify = | |
| existingModifiedFile || currentFiles.find((f) => f.path === filename); | |
| if (fileToModify && fileToModify.content) { | |
| const newContent = fileToModify.content.replace( | |
| searchText, | |
| replaceText | |
| ); | |
| if (newContent !== fileToModify.content) { | |
| modifiedFiles.set(filename, { path: filename, content: newContent }); | |
| } | |
| } else if (!fileToModify) { | |
| modifiedFiles.set(filename, { path: filename, content: replaceText }); | |
| } | |
| } | |
| } | |
| // Handle new file creation blocks | |
| if (message.includes(START_FILE_CONTENT)) { | |
| const fileSections = message.split(START_FILE_CONTENT).slice(1); | |
| for (const section of fileSections) { | |
| const rawContent = section.split(END_FILE_CONTENT)[0] || ""; | |
| // Extract filename - it's on the same line as START_FILE_CONTENT (before first newline) | |
| const firstNewlineIndex = rawContent.indexOf("\n"); | |
| let path = ""; | |
| let contentAfterFilename = rawContent; | |
| if (firstNewlineIndex !== -1) { | |
| // Filename is before the first newline (same line as START_FILE_CONTENT marker) | |
| path = rawContent.substring(0, firstNewlineIndex).trim(); | |
| contentAfterFilename = rawContent.substring(firstNewlineIndex + 1); | |
| } else { | |
| // No newline found, entire content might be just the filename | |
| path = rawContent.trim(); | |
| contentAfterFilename = ""; | |
| } | |
| // Validate that path has an extension | |
| const validatedPath = validateFilename(path); | |
| if (!validatedPath) continue; | |
| path = validatedPath; | |
| // Find code block | |
| const codeBlockStart = contentAfterFilename.indexOf("```"); | |
| if (codeBlockStart === -1) { | |
| // No code block, use raw content after filename | |
| const content = contentAfterFilename.trim(); | |
| if (content || !currentFiles.find((f) => f.path === path)) { | |
| // Always add files from the message | |
| modifiedFiles.set(path, { path, content }); | |
| } | |
| } else { | |
| // Has code block - extract content between ``` markers | |
| const afterCodeBlockStart = | |
| contentAfterFilename.substring(codeBlockStart); | |
| // Try to match complete code block first | |
| const completeMatch = afterCodeBlockStart.match( | |
| /^```[\w]*\n?([\s\S]*?)```/ | |
| ); | |
| if (completeMatch) { | |
| const content = completeMatch[1].trim(); | |
| modifiedFiles.set(path, { path, content }); | |
| } else { | |
| // Incomplete code block (streaming) - match everything after opening ``` | |
| const incompleteMatch = afterCodeBlockStart.match( | |
| /^```[\w]*\n?([\s\S]*)/ | |
| ); | |
| if (incompleteMatch) { | |
| let content = incompleteMatch[1]; | |
| // Remove trailing partial closing marker if present | |
| for (let i = 1; i < 3; i++) { | |
| const partialClosing = "`".repeat(i); | |
| if (content.endsWith(partialClosing)) { | |
| content = content.substring(0, content.length - i); | |
| break; | |
| } | |
| } | |
| content = content.trim(); | |
| modifiedFiles.set(path, { path, content }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Convert modified files map to array | |
| const files = Array.from(modifiedFiles.values())?.filter( | |
| (file) => file.path !== "README.md" | |
| ); | |
| return { | |
| messageContent, | |
| files, | |
| projectTitle, | |
| }; | |
| }; | |