import { createTwoFilesPatch } from 'diff'; import type { FileMap } from '~/lib/stores/files'; import { MODIFICATIONS_TAG_NAME } 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; export function computeFileModifications(files: FileMap, modifiedFiles: Map) { 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; } /** * Converts the unified diff to HTML. * * Example: * * ```html * * * - console.log('Hello, World!'); * + console.log('Hello, Bolt!'); * * * ``` */ 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, ``); } result.push(``); return result.join('\n'); }