Spaces:
Running
Running
| import { createTwoFilesPatch } from 'diff'; | |
| import type { FileMap } from '~/lib/stores/files'; | |
| import { MODIFICATIONS_TAG_NAME, WORK_DIR } from './constants'; | |
| export const modificationsRegex = new RegExp( | |
| `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`, | |
| 'g', | |
| ); | |
| interface ModifiedFile { | |
| type: 'diff' | 'file'; | |
| content: string; | |
| } | |
| type FileModifications = Record<string, ModifiedFile>; | |
| export function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) { | |
| const modifications: FileModifications = {}; | |
| let hasModifiedFiles = false; | |
| for (const [filePath, originalContent] of modifiedFiles) { | |
| const file = files[filePath]; | |
| if (file?.type !== 'file') { | |
| continue; | |
| } | |
| const unifiedDiff = diffFiles(filePath, originalContent, file.content); | |
| if (!unifiedDiff) { | |
| // files are identical | |
| continue; | |
| } | |
| hasModifiedFiles = true; | |
| if (unifiedDiff.length > file.content.length) { | |
| // if there are lots of changes we simply grab the current file content since it's smaller than the diff | |
| modifications[filePath] = { type: 'file', content: file.content }; | |
| } else { | |
| // otherwise we use the diff since it's smaller | |
| modifications[filePath] = { type: 'diff', content: unifiedDiff }; | |
| } | |
| } | |
| if (!hasModifiedFiles) { | |
| return undefined; | |
| } | |
| return modifications; | |
| } | |
| /** | |
| * Computes a diff in the unified format. The only difference is that the header is omitted | |
| * because it will always assume that you're comparing two versions of the same file and | |
| * it allows us to avoid the extra characters we send back to the llm. | |
| * | |
| * @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html | |
| */ | |
| export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) { | |
| let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent); | |
| const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`; | |
| const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd); | |
| if (headerEndIndex >= 0) { | |
| unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length); | |
| } | |
| if (unifiedDiff === '') { | |
| return undefined; | |
| } | |
| return unifiedDiff; | |
| } | |
| const regex = new RegExp(`^${WORK_DIR}\/`); | |
| /** | |
| * Strips out the work directory from the file path. | |
| */ | |
| export function extractRelativePath(filePath: string) { | |
| return filePath.replace(regex, ''); | |
| } | |
| /** | |
| * Converts the unified diff to HTML. | |
| * | |
| * Example: | |
| * | |
| * ```html | |
| * <bolt_file_modifications> | |
| * <diff path="/home/project/index.js"> | |
| * - console.log('Hello, World!'); | |
| * + console.log('Hello, Bolt!'); | |
| * </diff> | |
| * </bolt_file_modifications> | |
| * ``` | |
| */ | |
| export function fileModificationsToHTML(modifications: FileModifications) { | |
| const entries = Object.entries(modifications); | |
| if (entries.length === 0) { | |
| return undefined; | |
| } | |
| const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`]; | |
| for (const [filePath, { type, content }] of entries) { | |
| result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`); | |
| } | |
| result.push(`</${MODIFICATIONS_TAG_NAME}>`); | |
| return result.join('\n'); | |
| } | |