diff --git a/build.gradle.kts b/build.gradle.kts index 3a028cfe..9149c457 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ group = properties("libraryGroup") version = properties("libraryVersion") //plugins { -// id("org.jetbrains.kotlin.jvm") version "1.9.21" +// id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" //} tasks { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 101e4712..2cdada91 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -8,7 +8,7 @@ version = properties("libraryVersion") plugins { java `java-library` - id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" `maven-publish` id("signing") } diff --git a/gradle.properties b/gradle.properties index 41430921..e82aa896 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup = com.simiacryptus.skyenet -libraryVersion = 1.0.60 +libraryVersion = 1.0.61 gradleVersion = 7.6.1 diff --git a/groovy/build.gradle.kts b/groovy/build.gradle.kts index 0fd09d5d..10a6708c 100644 --- a/groovy/build.gradle.kts +++ b/groovy/build.gradle.kts @@ -8,7 +8,7 @@ version = properties("libraryVersion") plugins { java `java-library` - id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" `maven-publish` id("signing") } @@ -26,7 +26,7 @@ kotlin { jvmToolchain(11) } -val kotlin_version = "1.9.21" +val kotlin_version = "2.0.0-Beta5" dependencies { implementation(project(":core")) diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts index 71d16f5a..403a1199 100644 --- a/kotlin/build.gradle.kts +++ b/kotlin/build.gradle.kts @@ -8,7 +8,7 @@ version = properties("libraryVersion") plugins { java `java-library` - id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" `maven-publish` id("signing") } diff --git a/scala/build.gradle.kts b/scala/build.gradle.kts index 6d0ce9d0..7bd4308b 100644 --- a/scala/build.gradle.kts +++ b/scala/build.gradle.kts @@ -10,7 +10,7 @@ plugins { `java-library` `scala` `maven-publish` - id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" id("signing") } diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index 3e218d6f..fa367a0b 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -9,7 +9,7 @@ version = properties("libraryVersion") plugins { java `java-library` - id("org.jetbrains.kotlin.jvm") version "1.9.21" + id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5" `maven-publish` id("signing") id("io.freefair.sass-base") version "8.4" @@ -30,7 +30,7 @@ kotlin { // jvmToolchain(17) } -val kotlin_version = "1.9.21" +val kotlin_version = "2.0.0-Beta5" val jetty_version = "11.0.18" val jackson_version = "2.15.3" dependencies { diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt new file mode 100644 index 00000000..b4b6732f --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt @@ -0,0 +1,77 @@ +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.session.SocketManagerBase +import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown + +fun SocketManagerBase.addApplyDiffLinks( + code: StringBuilder, + response: String, + handle: (String) -> Unit, + task: SessionTask, + ui: ApplicationInterface? = null, +): String { + val diffPattern = """(?s)(? + val diffVal: String = diffBlock.groupValues[1] + val hrefLink = hrefLink("Apply Diff") { + try { + val newCode = PatchUtil.patch(code.toString(), diffVal).replace("\r", "") + handle(newCode) + task.complete("""
Diff Applied
""") + } catch (e: Throwable) { + task.error(ui, e) + } + } + val reverseHrefLink = hrefLink("(Bottom to Top)") { + try { + val reversedCode = code.lines().reversed().joinToString("\n") + val reversedDiff = diffVal.lines().reversed().joinToString("\n") + val newReversedCode = PatchUtil.patch(reversedCode, reversedDiff).replace("\r", "") + val newCode = newReversedCode.lines().reversed().joinToString("\n") + handle(newCode) + task.complete("""
Diff Applied (Bottom to Top)
""") + } catch (e: Throwable) { + task.error(ui, e) + } + } + val patch = PatchUtil.patch(code.toString(), diffVal).replace("\r", "") + val test1 = DiffUtil.formatDiff( + DiffUtil.generateDiff( + code.toString().replace("\r", "").lines(), + patch.lines() + ) + ) + val patchRev = PatchUtil.patch( + code.lines().reversed().joinToString("\n"), + diffVal.lines().reversed().joinToString("\n") + ).replace("\r", "") + val test2 = DiffUtil.formatDiff( + DiffUtil.generateDiff( + code.lines(), + patchRev.lines().reversed() + ) + ) + val newValue = if (patchRev == patch) { + displayMapInTabs( + mapOf( + "Diff" to renderMarkdown("```diff\n$diffVal\n```", ui = ui, tabs = true), + "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true), + ), ui = ui, split = true + ) + "\n" + hrefLink + } else { + displayMapInTabs( + mapOf( + "Diff" to renderMarkdown("```diff\n$diffVal\n```", ui = ui, tabs = true), + "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true), + "Reverse" to renderMarkdown("```diff\n$test2\n```", ui = ui, tabs = true), + ), ui = ui, split = true + ) + "\n" + hrefLink + "\n" + reverseHrefLink + } + markdown.replace(diffBlock.value, newValue) + } + return withLinks +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt new file mode 100644 index 00000000..c9771fca --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt @@ -0,0 +1,249 @@ +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.session.SocketManagerBase +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import java.io.File +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.readText + +fun SocketManagerBase.addApplyFileDiffLinks( + root: Path, + code: Map, + response: String, + handle: (Map) -> Unit, + task: SessionTask, + ui: ApplicationInterface, +): String { + val headerPattern = """(?s)(?> = diffPattern.findAll(response).map { it.range to it.groupValues[1] }.toList() + val codeblocks = codeblockPattern.findAll(response).filter { + when (it.groupValues[1]) { + "diff" -> false + else -> true + } + }.map { it.range to it }.toList() + val withPatchLinks: String = diffs.fold(response) { markdown, diffBlock -> + val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start } + val filename = header?.second ?: "Unknown" + val diffVal = diffBlock.second + val newValue = renderDiffBlock(root, filename, code, diffVal, handle, task, ui) + markdown.replace(diffVal, newValue) + } + val withSaveLinks = codeblocks.fold(withPatchLinks) { markdown, codeBlock -> + val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start } + val filename = header?.second ?: "Unknown" + val filepath = path(root, filename) + val prevCode = load(filepath, root, code) + val codeLang = codeBlock.second.groupValues[1] + val codeValue = codeBlock.second.groupValues[2] + val hrefLink = hrefLink("Save File") { + try { + handle( + mapOf( + filename to codeValue + ) + ) + task.complete("""
Saved ${filename}
""") + } catch (e: Throwable) { + task.error(null, e) + } + } + val codeblockRaw = """ + ```${codeLang} + ${codeValue} + ``` + """.trimIndent() + markdown.replace( + codeblockRaw, AgentPatterns.displayMapInTabs( + mapOf( + "New" to MarkdownUtil.renderMarkdown(codeblockRaw, ui = ui), + "Old" to MarkdownUtil.renderMarkdown( + """ + |```${codeLang} + |${prevCode} + |``` + """.trimMargin(), ui = ui + ), + "Patch" to MarkdownUtil.renderMarkdown( + """ + |```diff + |${ + DiffUtil.formatDiff( + DiffUtil.generateDiff( + prevCode.lines(), + codeValue.lines() + ) + ) + } + |``` + """.trimMargin(), ui = ui + ), + ) + ) + "\n" + hrefLink + ) + } + return withSaveLinks +} + + +private fun SocketManagerBase.renderDiffBlock( + root: Path, + filename: String, + code: Map, + diffVal: String, + handle: (Map) -> Unit, + task: SessionTask, + ui: ApplicationInterface +): String { + val filepath = path(root, filename) + val prevCode = load(filepath, root, code) + val newCode = PatchUtil.patch(prevCode, diffVal) + val echoDiff = try { + DiffUtil.formatDiff( + DiffUtil.generateDiff( + prevCode.lines(), + newCode.lines() + ) + ) + } catch (e: Throwable) { + MarkdownUtil.renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) + } + + val hrefLink = hrefLink("Apply Diff") { + try { + val relativize = try { + root.relativize(filepath) + } catch (e: Throwable) { + filepath + } + handle( + mapOf( + relativize.toString() to PatchUtil.patch( + prevCode, + diffVal + ) + ) + ) + task.complete("""
Diff Applied
""") + } catch (e: Throwable) { + task.error(null, e) + } + } + val reverseHrefLink = hrefLink("(Bottom to Top)") { + try { + val reversedCodeMap = code.mapValues { (_, v) -> v.lines().reversed().joinToString("\n") } + val reversedDiff = diffVal.lines().reversed().joinToString("\n") + val newReversedCodeMap = reversedCodeMap.mapValues { (file, prevCode) -> + if (filename == file) { + PatchUtil.patch(prevCode, reversedDiff).lines().reversed().joinToString("\n") + } else prevCode + } + handle(newReversedCodeMap) + task.complete("""
Diff Applied (Bottom to Top)
""") + } catch (e: Throwable) { + task.error(null, e) + } + } + val diffTask = ui?.newTask(root = false) + val prevCodeTask = ui?.newTask(root = false) + val newCodeTask = ui?.newTask(root = false) + val patchTask = ui?.newTask(root = false) + val inTabs = AgentPatterns.displayMapInTabs( + mapOf( + "Diff" to (diffTask?.placeholder ?: ""), + "Code" to (prevCodeTask?.placeholder ?: ""), + "Preview" to (newCodeTask?.placeholder ?: ""), + "Echo" to (patchTask?.placeholder ?: ""), + ) + ) + SocketManagerBase.scheduledThreadPoolExecutor.schedule({ + diffTask?.add(MarkdownUtil.renderMarkdown(/*escapeHtml4*/(diffVal), ui = ui)) + newCodeTask?.add( + MarkdownUtil.renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```", + ui = ui + ) + ) + prevCodeTask?.add( + MarkdownUtil.renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", + ui = ui + ) + ) + patchTask?.add(MarkdownUtil.renderMarkdown("# $filename\n\n```diff\n ${echoDiff}\n```", ui = ui)) + }, 100, TimeUnit.MILLISECONDS) + val newValue = inTabs + "\n" + hrefLink + "\n" + reverseHrefLink + return newValue +} + + +private fun load( + filepath: Path?, + root: Path, + code: Map +) = try { + if (true != filepath?.toFile()?.exists()) { + log.warn( + """ + |File not found: $filepath + |Root: ${root.toAbsolutePath()} + |Files: + |${code.keys.joinToString("\n") { "* $it" }} + """.trimMargin() + ) + "" + } else { + filepath.readText(Charsets.UTF_8) + } +} catch (e: Throwable) { + log.error("Error reading file: $filepath", e) + "" +} + +private fun path(root: Path, filename: String): Path? { + val filepath = try { + findFile(root, filename) ?: root.resolve(filename) + } catch (e: Throwable) { + log.error("Error finding file: $filename", e) + try { + root.resolve(filename) + } catch (e: Throwable) { + log.error("Error resolving file: $filename", e) + File(filename).toPath() + } + } + return filepath +} + +fun findFile(root: Path, filename: String): Path? { + return try { + when { + /* filename is absolute */ + filename.startsWith("/") -> { + val resolve = File(filename) + if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix("/")) + } + /* win absolute */ + filename.indexOf(":\\") == 1 -> { + val resolve = File(filename) + if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix(filename.substring(0, 2))) + } + + root.resolve(filename).toFile().exists() -> root.resolve(filename) + null != root.parent && root != root.parent -> findFile(root.parent, filename) + else -> null + } + } catch (e: Throwable) { + log.error("Error finding file: $filename", e) + null + } +} + +private val log = org.slf4j.LoggerFactory.getLogger(PatchUtil::class.java) \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt new file mode 100644 index 00000000..c46f8e8b --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt @@ -0,0 +1,28 @@ +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.session.SocketManagerBase + +fun SocketManagerBase.addSaveLinks( + response: String, + task: SessionTask, + handle: (String, String) -> Unit +): String { + val diffPattern = + """(?s)(? + val filename = diffBlock.groupValues[1] + val codeValue = diffBlock.groupValues[2] + val hrefLink = hrefLink("Save File") { + try { + handle(filename, codeValue) + task.complete("""
Saved ${filename}
""") + } catch (e: Throwable) { + task.error(null, e) + } + } + markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + hrefLink) + } + return withLinks +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt index 232a7e6e..23b0d304 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt @@ -1,19 +1,7 @@ package com.github.simiacryptus.aicoder.util -import com.github.simiacryptus.aicoder.util.ApxPatchUtil.patch -import com.github.simiacryptus.aicoder.util.DiffUtil.formatDiff -import com.github.simiacryptus.aicoder.util.DiffUtil.generateDiff -import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs -import com.simiacryptus.skyenet.webui.application.ApplicationInterface -import com.simiacryptus.skyenet.webui.session.SessionTask -import com.simiacryptus.skyenet.webui.session.SocketManagerBase -import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.apache.commons.text.similarity.LevenshteinDistance import org.slf4j.LoggerFactory -import java.io.File -import java.nio.file.Path -import java.util.concurrent.TimeUnit -import kotlin.io.path.readText object ApxPatchUtil { @@ -122,10 +110,10 @@ object ApxPatchUtil { private fun lineMatches( a: String, b: String, - factor: Double = 0.3 + factor: Double = 0.1, ): Boolean { val threshold = (Math.max(a.trim().length, b.trim().length) * factor).toInt() - val levenshteinDistance = LevenshteinDistance(5) + val levenshteinDistance = LevenshteinDistance(threshold+1) val dist = levenshteinDistance.apply(a.trim(), b.trim()) return if (dist >= 0) { dist <= threshold @@ -135,321 +123,5 @@ object ApxPatchUtil { } } -fun SocketManagerBase.addApplyDiffLinks( - code: StringBuilder, - response: String, - handle: (String) -> Unit, - task: SessionTask, - ui: ApplicationInterface? = null, -): String { - val diffPattern = """(?s)(? - val diffVal = diffBlock.value - val hrefLink = hrefLink("Apply Diff") { - try { - val newCode = patch(code.toString(), diffVal) - handle(newCode) - task.complete("""
Diff Applied
""") - } catch (e: Throwable) { - task.error(ui, e) - } - } - val reverseHrefLink = hrefLink("(Bottom to Top)") { - try { - val reversedCode = code.lines().reversed().joinToString("\n") - val reversedDiff = diffVal.lines().reversed().joinToString("\n") - val newReversedCode = patch(reversedCode, reversedDiff) - val newCode = newReversedCode.lines().reversed().joinToString("\n") - handle(newCode) - task.complete("""
Diff Applied (Bottom to Top)
""") - } catch (e: Throwable) { - task.error(ui, e) - } - } - val test1 = formatDiff( - generateDiff( - code.lines(), - patch(code.toString(), diffVal).lines() - ) - ) - val test2 = formatDiff( - generateDiff( - code.lines(), - patch( - code.lines().reversed().joinToString("\n"), - diffVal.lines().reversed().joinToString("\n") - ).lines().reversed() - ) - ) - val newValue = displayMapInTabs( - mapOf( - "Diff" to renderMarkdown(diffVal, ui = ui, tabs = true), - "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true), - "Reverse" to renderMarkdown("```diff\n$test2\n```", ui = ui, tabs = true), - ), ui = ui - ) + "\n" + hrefLink + "\n" + reverseHrefLink - markdown.replace(diffVal, newValue) - } - return withLinks -} - -fun SocketManagerBase.addSaveLinks( - response: String, - task: SessionTask, - handle: (String, String) -> Unit -): String { - val diffPattern = - """(?s)(? - val filename = diffBlock.groupValues[1] - val codeValue = diffBlock.groupValues[2] - val hrefLink = hrefLink("Save File") { - try { - handle(filename, codeValue) - task.complete("""
Saved ${filename}
""") - } catch (e: Throwable) { - task.error(null, e) - } - } - markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + hrefLink) - } - return withLinks -} - private val log = LoggerFactory.getLogger(ApxPatchUtil::class.java) -fun findFile(root: Path, filename: String): Path? { - return try { - when { - /* filename is absolute */ - filename.startsWith("/") -> { - val resolve = File(filename) - if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix("/")) - } - /* win absolute */ - filename.indexOf(":\\") == 1 -> { - val resolve = File(filename) - if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix(filename.substring(0, 2))) - } - - root.resolve(filename).toFile().exists() -> root.resolve(filename) - null != root.parent && root != root.parent -> findFile(root.parent, filename) - else -> null - } - } catch (e: Throwable) { - log.error("Error finding file: $filename", e) - null - } -} - -fun SocketManagerBase.addApplyDiffLinks2( - root: Path, - code: Map, - response: String, - handle: (Map) -> Unit, - task: SessionTask, - ui: ApplicationInterface, -): String { - val headerPattern = """(?s)(?> = diffPattern.findAll(response).map { it.range to it.groupValues[1] }.toList() - val codeblocks = codeblockPattern.findAll(response).filter { - when (it.groupValues[1]) { - "diff" -> false - else -> true - } - }.map { it.range to it }.toList() - val withPatchLinks: String = diffs.fold(response) { markdown, diffBlock -> - val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start } - val filename = header?.second ?: "Unknown" - val diffVal = diffBlock.second - val newValue = renderDiffBlock(root, filename, code, diffVal, handle, task, ui) - markdown.replace(diffVal, newValue) - } - val withSaveLinks = codeblocks.fold(withPatchLinks) { markdown, codeBlock -> - val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start } - val filename = header?.second ?: "Unknown" - val filepath = path(root, filename) - val prevCode = load(filepath, root, code) - val codeLang = codeBlock.second.groupValues[1] - val codeValue = codeBlock.second.groupValues[2] - val hrefLink = hrefLink("Save File") { - try { - handle( - mapOf( - filename to codeValue - ) - ) - task.complete("""
Saved ${filename}
""") - } catch (e: Throwable) { - task.error(null, e) - } - } - val codeblockRaw = """ - ```${codeLang} - ${codeValue} - ``` - """.trimIndent() - markdown.replace( - codeblockRaw, displayMapInTabs( - mapOf( - "New" to renderMarkdown(codeblockRaw, ui = ui), - "Old" to renderMarkdown( - """ - |```${codeLang} - |${prevCode} - |``` - """.trimMargin(), ui = ui - ), - "Patch" to renderMarkdown( - """ - |```diff - |${ - formatDiff( - generateDiff( - prevCode.lines(), - codeValue.lines() - ) - ) - } - |``` - """.trimMargin(), ui = ui - ), - ) - ) + "\n" + hrefLink - ) - } - return withSaveLinks -} - -private fun SocketManagerBase.renderDiffBlock( - root: Path, - filename: String, - code: Map, - diffVal: String, - handle: (Map) -> Unit, - task: SessionTask, - ui: ApplicationInterface -): String { - val filepath = path(root, filename) - val prevCode = load(filepath, root, code) - val newCode = patch(prevCode, diffVal) - val echoDiff = try { - formatDiff( - generateDiff( - prevCode.lines(), - newCode.lines() - ) - ) - } catch (e: Throwable) { - renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui) - } - - val hrefLink = hrefLink("Apply Diff") { - try { - val relativize = try { - root.relativize(filepath) - } catch (e: Throwable) { - filepath - } - handle( - mapOf( - relativize.toString() to patch( - prevCode, - diffVal - ) - ) - ) - task.complete("""
Diff Applied
""") - } catch (e: Throwable) { - task.error(null, e) - } - } - val reverseHrefLink = hrefLink("(Bottom to Top)") { - try { - val reversedCodeMap = code.mapValues { (_, v) -> v.lines().reversed().joinToString("\n") } - val reversedDiff = diffVal.lines().reversed().joinToString("\n") - val newReversedCodeMap = reversedCodeMap.mapValues { (file, prevCode) -> - if (filename == file) { - patch(prevCode, reversedDiff).lines().reversed().joinToString("\n") - } else prevCode - } - handle(newReversedCodeMap) - task.complete("""
Diff Applied (Bottom to Top)
""") - } catch (e: Throwable) { - task.error(null, e) - } - } - val diffTask = ui?.newTask(root = false) - val prevCodeTask = ui?.newTask(root = false) - val newCodeTask = ui?.newTask(root = false) - val patchTask = ui?.newTask(root = false) - val inTabs = displayMapInTabs( - mapOf( - "Diff" to (diffTask?.placeholder ?: ""), - "Code" to (prevCodeTask?.placeholder ?: ""), - "Preview" to (newCodeTask?.placeholder ?: ""), - "Echo" to (patchTask?.placeholder ?: ""), - ) - ) - SocketManagerBase.scheduledThreadPoolExecutor.schedule({ - diffTask?.add(renderMarkdown(/*escapeHtml4*/(diffVal), ui = ui)) - newCodeTask?.add( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```", - ui = ui - ) - ) - prevCodeTask?.add( - renderMarkdown( - "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", - ui = ui - ) - ) - patchTask?.add(renderMarkdown("# $filename\n\n```diff\n ${echoDiff}\n```", ui = ui)) - }, 100, TimeUnit.MILLISECONDS) - val newValue = inTabs + "\n" + hrefLink + "\n" + reverseHrefLink - return newValue -} - -private fun load( - filepath: Path?, - root: Path, - code: Map -) = try { - if (true != filepath?.toFile()?.exists()) { - log.warn( - """ - |File not found: $filepath - |Root: ${root.toAbsolutePath()} - |Files: - |${code.keys.joinToString("\n") { "* $it" }} - """.trimMargin() - ) - "" - } else { - filepath.readText(Charsets.UTF_8) - } -} catch (e: Throwable) { - log.error("Error reading file: $filepath", e) - "" -} - -private fun path(root: Path, filename: String): Path? { - val filepath = try { - findFile(root, filename) ?: root.resolve(filename) - } catch (e: Throwable) { - log.error("Error finding file: $filename", e) - try { - root.resolve(filename) - } catch (e: Throwable) { - log.error("Error resolving file: $filename", e) - File(filename).toPath() - } - } - return filepath -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt new file mode 100644 index 00000000..16d20ffe --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt @@ -0,0 +1,196 @@ +package com.github.simiacryptus.aicoder.util + +import org.apache.commons.text.similarity.LevenshteinDistance +import org.slf4j.LoggerFactory + +object IterativePatchUtil { + + enum class LineType { CONTEXT, ADD, DELETE } + class LineRecord( + val index: Int, + val line: String, + val previousLine: LineRecord? = null, + val nextLine: LineRecord? = null, + var matchingLine: LineRecord? = null, + var type: LineType = LineType.CONTEXT + ) + + fun patch(source: String, patch: String): String { + val sourceLines = parseLines(source) + val patchLines = parsePatchLines(patch) + + // Step 1: Link all unique lines in the source and patch that match exactly + linkUniqueMatchingLines(sourceLines, patchLines) + + // Step 2: Link all exact matches in the source and patch which are adjacent to established links + linkAdjacentMatchingLines(sourceLines, patchLines) + + // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links. + // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found. + linkByLevenshteinDistance(sourceLines, patchLines) + + // Generate the patched text + return generatePatchedText(sourceLines, patchLines) + } + + private fun generatePatchedText(sourceLines: List, patchLines: List): String { + val patchedTextBuilder = StringBuilder() + + // Add unmatched code lines at the beginning + sourceLines + .takeWhile { it.matchingLine == null } + .forEach { patchedTextBuilder.appendln(it.line) } + + // Iterate through patch lines and apply changes to the source + patchLines.forEach { patchLine -> + when (patchLine.type) { + LineType.ADD -> patchedTextBuilder.appendln(patchLine.line) + LineType.DELETE -> { + // Skip adding the line to the patched text + } + + LineType.CONTEXT -> { + // For context lines, we need to find the corresponding line in the source + // If there's a matching line, we add the source line to maintain original formatting + // If not, we add the patch line (it could be a case where context lines don't match exactly due to trimming) + val sourceLine = sourceLines.find { it.index == patchLine.index && it.matchingLine == patchLine } + if (sourceLine != null) { + patchedTextBuilder.appendln(sourceLine.line) + } else { + patchedTextBuilder.appendln(patchLine.line) + } + } + } + } + + // Add unmatched code lines at the end + sourceLines.reversed() + .takeWhile { it.matchingLine == null } + .reversed() + .forEach { patchedTextBuilder.appendln(it.line) } + + return patchedTextBuilder.toString().trimEnd() + } + + private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) { + val sourceLineMap = sourceLines.groupBy { it.line.trim() } + val patchLineMap = patchLines.groupBy { it.line.trim() } + + sourceLineMap.keys.intersect(patchLineMap.keys).forEach { key -> + val sourceLine = sourceLineMap[key]?.singleOrNull() + val patchLine = patchLineMap[key]?.singleOrNull() + if (sourceLine != null && patchLine != null) { + sourceLine.matchingLine = patchLine + patchLine.matchingLine = sourceLine + } + } + } + + private fun linkAdjacentMatchingLines(sourceLines: List, patchLines: List) { + var foundMatch = true + while (foundMatch) { + foundMatch = false + for (sourceLine in sourceLines) { + val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line + + // Check the previous line + if (sourceLine.previousLine != null && patchLine.previousLine != null) { + val sourcePrev = sourceLine.previousLine + val patchPrev = patchLine.previousLine + if (sourcePrev.line.trim() == patchPrev.line.trim() && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { + sourcePrev.matchingLine = patchPrev + patchPrev.matchingLine = sourcePrev + foundMatch = true + } + } + + // Check the next line + if (sourceLine.nextLine != null && patchLine.nextLine != null) { + val sourceNext = sourceLine.nextLine + val patchNext = patchLine.nextLine + if (sourceNext.line.trim() == patchNext.line.trim() && sourceNext.matchingLine == null && patchNext.matchingLine == null) { + sourceNext.matchingLine = patchNext + patchNext.matchingLine = sourceNext + foundMatch = true + } + } + } + } + } + + private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) { + val levenshteinDistance = LevenshteinDistance() + val maxDistance = 5 // Define a maximum acceptable distance. Adjust as needed. + + // Iterate over source lines to find potential matches in the patch lines + for (sourceLine in sourceLines) { + if (sourceLine.matchingLine != null) continue // Skip lines that already have matches + + var bestMatch: LineRecord? = null + var bestDistance = Int.MAX_VALUE + var bestCombinedDistance = Int.MAX_VALUE + + for (patchLine in patchLines) { + if (patchLine.matchingLine != null) continue // Skip lines that already have matches + + val distance = levenshteinDistance.apply(sourceLine.line.trim(), patchLine.line.trim()) + if (distance <= maxDistance) { + // Calculate combined distance, factoring in proximity to established links + val combinedDistance = distance + calculateProximityDistance(sourceLine, patchLine) + + if (combinedDistance < bestCombinedDistance) { + bestMatch = patchLine + bestDistance = distance + bestCombinedDistance = combinedDistance + } + } + } + + if (bestMatch != null) { + // Establish the best match + sourceLine.matchingLine = bestMatch + bestMatch.matchingLine = sourceLine + } + } + } + + private fun calculateProximityDistance(sourceLine: LineRecord, patchLine: LineRecord): Int { + // Implement logic to calculate proximity distance based on the distance to the nearest established link + // This is a simplified example. You may need a more sophisticated approach based on your specific requirements. + var distance = 0 + if (sourceLine.previousLine?.matchingLine != null || patchLine.previousLine?.matchingLine != null) { + distance += 1 + } + if (sourceLine.nextLine?.matchingLine != null || patchLine.nextLine?.matchingLine != null) { + distance += 1 + } + return distance + } + + private fun parseLines(text: String): List { + return text.lines().mapIndexed { index, line -> + LineRecord(index, line) + } + } + + private fun parsePatchLines(text: String): List { + return text.lines().mapIndexed { index, line -> + LineRecord( + index = index, line = line.let { + when { + it.trimStart().startsWith("+") -> it.trimStart().substring(1) + it.trimStart().startsWith("-") -> it.trimStart().substring(1) + else -> it + } + }, type = when { + line.startsWith("+") -> LineType.ADD + line.startsWith("-") -> LineType.DELETE + else -> LineType.CONTEXT + } + ) + } + } + + private val log = LoggerFactory.getLogger(ApxPatchUtil::class.java) +} + diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt new file mode 100644 index 00000000..07204caf --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt @@ -0,0 +1,3 @@ +package com.github.simiacryptus.aicoder.util + +typealias PatchUtil = IterativePatchUtil \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt index abb4710e..4e7c80a5 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt @@ -1,7 +1,7 @@ package com.simiacryptus.skyenet.apps.general import com.github.simiacryptus.aicoder.actions.generic.commonRoot -import com.github.simiacryptus.aicoder.util.addApplyDiffLinks2 +import com.github.simiacryptus.aicoder.util.addApplyFileDiffLinks import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.Role @@ -13,7 +13,6 @@ import com.simiacryptus.jopenai.util.JsonUtil import com.simiacryptus.skyenet.Acceptable import com.simiacryptus.skyenet.AgentPatterns import com.simiacryptus.skyenet.core.actors.* -import com.simiacryptus.skyenet.core.actors.CodingActor.Companion.indent import com.simiacryptus.skyenet.core.platform.ClientManager import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.StorageInterface @@ -249,7 +248,7 @@ class WebDevAgent( fun outputFn(task: SessionTask, design: String): StringBuilder? { //val task = ui.newTask() return task.complete( - ui.socketManager.addApplyDiffLinks2( + ui.socketManager.addApplyFileDiffLinks( root = codeFiles.keys.map { File(it).toPath() }.toTypedArray().commonRoot(), code = codeFiles, response = design, diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index d958a00a..5243df40 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -96,7 +96,7 @@ let loadImages = "true"; if (this.isPanning === false) return; const dx = event.clientX - this.startX; const dy = event.clientY - this.startY; - if(this.priorPan) { + if (this.priorPan) { if (this.currentTransform.x) { this.currentTransform.x = dx * moveScale + this.priorPan.x; } else { @@ -146,7 +146,7 @@ function applyToAllSvg() { function substituteMessages(outerMessageId, messageDiv) { Object.entries(messageMap).forEach(([innerMessageId, content]) => { - if(outerMessageId !== innerMessageId && messageDiv) messageDiv.querySelectorAll('[id="' + innerMessageId + '"]').forEach((element) => { + if (outerMessageId !== innerMessageId && messageDiv) messageDiv.querySelectorAll('[id="' + innerMessageId + '"]').forEach((element) => { if (element.innerHTML !== content) { //console.log("Substituting message with id " + innerMessageId + " and content " + content); element.innerHTML = content; @@ -169,8 +169,8 @@ function onWebSocketText(event) { // console.log("Ignoring message with id " + messageId + " and version " + messageVersion); // return; // } else { - messageVersions[messageId] = messageVersion; - messageMap[messageId] = messageContent; + messageVersions[messageId] = messageVersion; + messageMap[messageId] = messageContent; // } // Cleanup: remove temporary event listeners @@ -181,7 +181,7 @@ function onWebSocketText(event) { substituteMessages(messageId, messageDiv); } }); - if(messageDivs.length == 0 && !messageId.startsWith("z")) { + if (messageDivs.length == 0 && !messageId.startsWith("z")) { messageDiv = document.createElement('div'); messageDiv.className = 'message message-container'; // Add the message-container class messageDiv.id = messageId; @@ -209,7 +209,7 @@ function onWebSocketText(event) { } } if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; - try{ + try { if (typeof Prism !== 'undefined') Prism.highlightAll(); } catch (e) { console.log("Error highlighting code: " + e); @@ -247,6 +247,8 @@ function updateTabs() { event.stopPropagation(); const forTab = button.getAttribute('data-for-tab'); let tabsParent = button.closest('.tabs-container'); + tabsParent.querySelectorAll('.tab-button').forEach(tabButton => tabButton.classList.remove('active')); + button.classList.add('active'); tabsParent.querySelectorAll('.tab-content').forEach(content => { const contentParent = content.closest('.tabs-container'); if (contentParent === tabsParent) { diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt new file mode 100644 index 00000000..971cd095 --- /dev/null +++ b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt @@ -0,0 +1,38 @@ +package com.github.simiacryptus.aicoder.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class ApxPatchUtilTest { + + @Test + fun `test patch with simple addition`() { + val source = "Line 1\nLine 2\nLine 3" + val patch = """ + +++ + @@ -1,3 +1,4 @@ + +Added Line + Line 1 + Line 2 + Line 3 + """.trimIndent() + val expected = "Added Line\nLine 1\nLine 2\nLine 3" + val result = ApxPatchUtil.patch(source, patch) + assertEquals(expected, result) + } + + @Test + fun `test patch with deletion`() { + val source = "Line 1\nLine 2\nLine 3" + val patch = """ + --- + @@ -1,3 +1,2 @@ + -Line 2 + Line 1 + Line 3 + """.trimIndent() + val expected = "Line 1\nLine 3" + val result = ApxPatchUtil.patch(source, patch) + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt new file mode 100644 index 00000000..f4cc732b --- /dev/null +++ b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt @@ -0,0 +1,152 @@ +package com.github.simiacryptus.aicoder.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class IterativePatchUtilTest { + + @Test + fun testPatchExactMatch() { + val source = """ + line1 + line2 + line3 + """.trimIndent() + val patch = """ + line1 + line2 + line3 + """.trimIndent() + val result = IterativePatchUtil.patch(source, patch) + assertEquals(source.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchAddLine() { + val source = """ + line1 + line2 + line3 + """.trimIndent() + val patch = """ + line1 + line2 + +newLine + line3 + """.trimIndent() + val expected = """ + line1 + line2 + newLine + line3 + """.trimIndent() + val result = IterativePatchUtil.patch(source, patch) + assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchModifyLine() { + val source = """ + line1 + line2 + line3 + """.trimIndent() + val patch = """ + line1 + -line2 + +modifiedLine2 + line3 + """.trimIndent() + val expected = """ + line1 + modifiedLine2 + line3 + """.trimIndent() + val result = IterativePatchUtil.patch(source, patch) + assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + + @Test + fun testPatchRemoveLine() { + val source = """ + line1 + line2 + line3 + """.trimIndent() + val patch = """ + line1 + line3 + """.trimIndent() + val expected = """ + line1 + line3 + """.trimIndent() + val result = IterativePatchUtil.patch(source, patch) + assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + } + @Test + fun testFromData() { + val source = """ + function updateTabs() { + document.querySelectorAll('.tab-button').forEach(button => { + button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter + event.stopPropagation(); + const forTab = button.getAttribute('data-for-tab'); + let tabsParent = button.closest('.tabs-container'); + tabsParent.querySelectorAll('.tab-content').forEach(content => { + const contentParent = content.closest('.tabs-container'); + if (contentParent === tabsParent) { + if (content.getAttribute('data-tab') === forTab) { + content.classList.add('active'); + } else if (content.classList.contains('active')) { + content.classList.remove('active') + } + } + }); + }) + }); + } + """.trimIndent() + val patch = """ + tabsParent.querySelectorAll('.tab-content').forEach(content => { + const contentParent = content.closest('.tabs-container'); + if (contentParent === tabsParent) { + if (content.getAttribute('data-tab') === forTab) { + content.classList.add('active'); + + button.classList.add('active'); // Mark the button as active + } else if (content.classList.contains('active')) { + content.classList.remove('active') + + button.classList.remove('active'); // Ensure the button is not marked as active + } + } + }); + """.trimIndent() + val expected = """ + function updateTabs() { + document.querySelectorAll('.tab-button').forEach(button => { + button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter + event.stopPropagation(); + const forTab = button.getAttribute('data-for-tab'); + let tabsParent = button.closest('.tabs-container'); + tabsParent.querySelectorAll('.tab-content').forEach(content => { + const contentParent = content.closest('.tabs-container'); + if (contentParent === tabsParent) { + if (content.getAttribute('data-tab') === forTab) { + content.classList.add('active'); + button.classList.add('active'); // Mark the button as active + } else if (content.classList.contains('active')) { + content.classList.remove('active') + button.classList.remove('active'); // Ensure the button is not marked as active + } + } + }); + }) + }); + } + """.trimIndent() + val result = IterativePatchUtil.patch(source, patch) + assertEquals( + expected.replace("\r\n", "\n").replace("\\s{1,}".toRegex(), " "), + result.replace("\r\n", "\n").replace("\\s{1,}".toRegex(), " ")) + } +} \ No newline at end of file