From 74b7c54c7c48fed452b7086cad3cfda498fddb0b Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 14 Apr 2024 15:58:25 -0400 Subject: [PATCH] 1.0.62 (#67) * 1.0.62 * revised patching logic * Update MarkdownUtil.kt * Update MarkdownUtil.kt * update mermaid fixups and rendering * Update main.js * Improved ui for save/patch operations * showMenubar --- core/build.gradle.kts | 2 +- gradle.properties | 2 +- webui/build.gradle.kts | 2 +- .../aicoder/util/AddApplyDiffLinks.kt | 77 --- .../aicoder/util/AddApplyFileDiffLinks.kt | 249 -------- .../aicoder/util/IterativePatchUtil.kt | 196 ------- .../simiacryptus/aicoder/util/PatchUtil.kt | 3 - .../simiacryptus/diff/AddApplyDiffLinks.kt | 87 +++ .../diff/AddApplyFileDiffLinks.kt | 263 +++++++++ .../{aicoder/util => diff}/AddSaveLinks.kt | 19 +- .../{aicoder/util => diff}/ApxPatchUtil.kt | 2 +- .../{aicoder/util => diff}/DiffMatchPatch.kt | 10 +- .../{aicoder/util => diff}/DiffUtil.kt | 4 +- .../simiacryptus/diff/IterativePatchUtil.kt | 210 +++++++ .../com/github/simiacryptus/diff/PatchUtil.kt | 3 + .../com/simiacryptus/skyenet/Acceptable.kt | 2 +- .../skyenet/apps/general/WebDevApp.kt | 41 +- .../webui/application/ApplicationServer.kt | 263 ++++----- .../webui/session/SocketManagerBase.kt | 2 +- .../skyenet/webui/util/MarkdownUtil.kt | 212 +++++-- webui/src/main/resources/application/main.js | 58 +- webui/src/main/resources/shared/_main.scss | 546 ++++++++++-------- .../util => diff}/ApxPatchUtilTest.kt | 8 +- .../{aicoder/util => diff}/DiffUtilTest.kt | 24 +- .../util => diff}/IterativePatchUtilTest.kt | 20 +- .../skyenet/webui/util/MarkdownUtilTest.kt | 34 ++ 26 files changed, 1311 insertions(+), 1028 deletions(-) delete mode 100644 webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt delete mode 100644 webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt delete mode 100644 webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt delete mode 100644 webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt create mode 100644 webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyDiffLinks.kt create mode 100644 webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyFileDiffLinks.kt rename webui/src/main/kotlin/com/github/simiacryptus/{aicoder/util => diff}/AddSaveLinks.kt (55%) rename webui/src/main/kotlin/com/github/simiacryptus/{aicoder/util => diff}/ApxPatchUtil.kt (98%) rename webui/src/main/kotlin/com/github/simiacryptus/{aicoder/util => diff}/DiffMatchPatch.kt (99%) rename webui/src/main/kotlin/com/github/simiacryptus/{aicoder/util => diff}/DiffUtil.kt (97%) create mode 100644 webui/src/main/kotlin/com/github/simiacryptus/diff/IterativePatchUtil.kt create mode 100644 webui/src/main/kotlin/com/github/simiacryptus/diff/PatchUtil.kt rename webui/src/test/kotlin/com/github/simiacryptus/{aicoder/util => diff}/ApxPatchUtilTest.kt (82%) rename webui/src/test/kotlin/com/github/simiacryptus/{aicoder/util => diff}/DiffUtilTest.kt (86%) rename webui/src/test/kotlin/com/github/simiacryptus/{aicoder/util => diff}/IterativePatchUtilTest.kt (88%) create mode 100644 webui/src/test/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtilTest.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 2cdada91..821e9d2c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -32,7 +32,7 @@ val jackson_version = "2.15.3" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.50") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.51") implementation("org.apache.commons:commons-text:1.11.0") diff --git a/gradle.properties b/gradle.properties index e82aa896..d13aca04 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.61 +libraryVersion = 1.0.62 gradleVersion = 7.6.1 diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index fa367a0b..43a22f83 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -35,7 +35,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.15.3" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.50") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.51") implementation(project(":core")) implementation(project(":kotlin")) 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 deleted file mode 100644 index b4b6732f..00000000 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index c9771fca..00000000 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt +++ /dev/null @@ -1,249 +0,0 @@ -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/IterativePatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt deleted file mode 100644 index 16d20ffe..00000000 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt +++ /dev/null @@ -1,196 +0,0 @@ -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 deleted file mode 100644 index 07204caf..00000000 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.simiacryptus.aicoder.util - -typealias PatchUtil = IterativePatchUtil \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyDiffLinks.kt new file mode 100644 index 00000000..13ef757a --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyDiffLinks.kt @@ -0,0 +1,87 @@ +package com.github.simiacryptus.diff + +import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs +import com.simiacryptus.skyenet.set +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, +): String { + val diffPattern = """(?s)(? + val diffVal: String = diffBlock.groupValues[1] + val applydiffTask = ui.newTask(false) + lateinit var hrefLink: StringBuilder + var reverseHrefLink: StringBuilder? = null + hrefLink = applydiffTask.complete(hrefLink("Apply Diff", classname = "href-link cmd-button") { + try { + val newCode = IterativePatchUtil.patch(code.toString(), diffVal).replace("\r", "") + handle(newCode) + reverseHrefLink?.clear() + hrefLink.set("""
Diff Applied
""") + applydiffTask.complete() + } catch (e: Throwable) { + task.error(ui, e) + } + })!! + val patch = IterativePatchUtil.patch(code.toString(), diffVal).replace("\r", "") + val test1 = DiffUtil.formatDiff( + DiffUtil.generateDiff( + code.toString().replace("\r", "").lines(), + patch.lines() + ) + ) + val patchRev = IterativePatchUtil.patch( + code.lines().reversed().joinToString("\n"), + diffVal.lines().reversed().joinToString("\n") + ).replace("\r", "") + if (patchRev != patch) { + reverseHrefLink = applydiffTask.complete(hrefLink("(Bottom to Top)", classname = "href-link cmd-button") { + try { + val reversedCode = code.lines().reversed().joinToString("\n") + val reversedDiff = diffVal.lines().reversed().joinToString("\n") + val newReversedCode = IterativePatchUtil.patch(reversedCode, reversedDiff).replace("\r", "") + val newCode = newReversedCode.lines().reversed().joinToString("\n") + handle(newCode) + hrefLink.clear() + reverseHrefLink!!.set("""
Diff Applied (Bottom to Top)
""") + applydiffTask.complete() + } catch (e: Throwable) { + task.error(ui, e) + } + })!! + } + 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" + applydiffTask.placeholder + } 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" + applydiffTask.placeholder + } + markdown.replace(diffBlock.value, newValue) + } + return withLinks +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyFileDiffLinks.kt new file mode 100644 index 00000000..d4e5c8f2 --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/AddApplyFileDiffLinks.kt @@ -0,0 +1,263 @@ +package com.github.simiacryptus.diff + +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.set +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +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, + 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, ui) + markdown.replace("```diff\n$diffVal\n```", 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 commandTask = ui.newTask(false) + lateinit var hrefLink: StringBuilder + hrefLink = commandTask.complete(hrefLink("Save File", classname = "href-link cmd-button") { + try { + handle( + mapOf( + filename to codeValue + ) + ) + hrefLink.set("""
Saved ${filename}
""") + commandTask.complete() + //task.complete("""
Saved ${filename}
""") + } catch (e: Throwable) { + commandTask.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" + commandTask.placeholder + ) + } + return withSaveLinks +} + + +private fun SocketManagerBase.renderDiffBlock( + root: Path, + filename: String, + code: Map, + diffVal: String, + handle: (Map) -> Unit, + ui: ApplicationInterface +): String { + val filepath = path(root, filename) + val prevCode = load(filepath, root, code) + val newCode = IterativePatchUtil.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 applydiffTask = ui.newTask(false) + lateinit var hrefLink: StringBuilder + lateinit var reverseHrefLink: StringBuilder + hrefLink = applydiffTask.complete(hrefLink("Apply Diff", classname = "href-link cmd-button") { + try { + val relativize = try { + root.relativize(filepath) + } catch (e: Throwable) { + filepath + } + handle( + mapOf( + relativize.toString() to IterativePatchUtil.patch( + prevCode, + diffVal + ) + ) + ) + reverseHrefLink.clear() + hrefLink.set("""
Diff Applied
""") + applydiffTask.complete() + } catch (e: Throwable) { + applydiffTask.error(null, e) + } + })!! + reverseHrefLink = applydiffTask.complete(hrefLink("(Bottom to Top)", classname = "href-link cmd-button") { + 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) { + IterativePatchUtil.patch(prevCode, reversedDiff).lines().reversed().joinToString("\n") + } else prevCode + } + handle(newReversedCodeMap) + hrefLink.clear() + reverseHrefLink.set("""
Diff Applied (Bottom to Top)
""") + applydiffTask.complete() + } catch (e: Throwable) { + applydiffTask.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?.complete(MarkdownUtil.renderMarkdown(/*escapeHtml4*/("```diff\n$diffVal\n```"), ui = ui)) + newCodeTask?.complete( + MarkdownUtil.renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```", + ui = ui + ) + ) + prevCodeTask?.complete( + MarkdownUtil.renderMarkdown( + "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```", + ui = ui + ) + ) + patchTask?.complete(MarkdownUtil.renderMarkdown("# $filename\n\n```diff\n ${echoDiff}\n```", ui = ui)) + }, 100, TimeUnit.MILLISECONDS) + val newValue = inTabs + "\n" + applydiffTask.placeholder + 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/diff/AddSaveLinks.kt similarity index 55% rename from webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt rename to webui/src/main/kotlin/com/github/simiacryptus/diff/AddSaveLinks.kt index c46f8e8b..15df0408 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/AddSaveLinks.kt @@ -1,12 +1,15 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff +import com.simiacryptus.skyenet.set +import com.simiacryptus.skyenet.webui.application.ApplicationInterface 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 + ui: ApplicationInterface, + handle: (String, String) -> Unit, ): String { val diffPattern = """(?s)(? val filename = diffBlock.groupValues[1] val codeValue = diffBlock.groupValues[2] - val hrefLink = hrefLink("Save File") { + val commandTask = ui.newTask(false) + lateinit var hrefLink: StringBuilder + hrefLink = commandTask.complete(hrefLink("Save File", classname = "href-link cmd-button") { try { handle(filename, codeValue) - task.complete("""
Saved ${filename}
""") + hrefLink.set("""
Saved ${filename}
""") + commandTask.complete() + //task.complete("""
Saved ${filename}
""") } catch (e: Throwable) { task.error(null, e) } - } - markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + hrefLink) + })!! + markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + commandTask.placeholder) } 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/diff/ApxPatchUtil.kt similarity index 98% rename from webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt rename to webui/src/main/kotlin/com/github/simiacryptus/diff/ApxPatchUtil.kt index 23b0d304..15601617 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/ApxPatchUtil.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff import org.apache.commons.text.similarity.LevenshteinDistance import org.slf4j.LoggerFactory diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatch.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/DiffMatchPatch.kt similarity index 99% rename from webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatch.kt rename to webui/src/main/kotlin/com/github/simiacryptus/diff/DiffMatchPatch.kt index 1210badc..1c38266a 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatch.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/DiffMatchPatch.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff import java.io.UnsupportedEncodingException import java.net.URLDecoder @@ -564,8 +564,8 @@ open class DiffMatchPatch { * @param lineArray List of unique strings. */ private fun diff_charsToLines( - diffs: List, - lineArray: List + diffs: List, + lineArray: List ) { var text: StringBuilder for (diff: Diff in diffs) { @@ -2271,11 +2271,11 @@ open class DiffMatchPatch { * @param operation One of INSERT, DELETE or EQUAL. * @param text The text being applied. */( - /** + /** * One of: INSERT, DELETE or EQUAL. */ var operation: Operation?, - /** + /** * The text associated with this diff operation. */ var text: String? diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/DiffUtil.kt similarity index 97% rename from webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffUtil.kt rename to webui/src/main/kotlin/com/github/simiacryptus/diff/DiffUtil.kt index 5bc8daa3..97791bae 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/DiffUtil.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/DiffUtil.kt @@ -1,6 +1,6 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff -import com.github.simiacryptus.aicoder.util.PatchLineType.* +import com.github.simiacryptus.diff.PatchLineType.* enum class PatchLineType { Added, Deleted, Unchanged diff --git a/webui/src/main/kotlin/com/github/simiacryptus/diff/IterativePatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/IterativePatchUtil.kt new file mode 100644 index 00000000..386255b7 --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/IterativePatchUtil.kt @@ -0,0 +1,210 @@ +package com.github.simiacryptus.diff + +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, + var previousLine: LineRecord? = null, + var nextLine: LineRecord? = null, + var matchingLine: LineRecord? = null, + var type: LineType = LineType.CONTEXT + ) { + override fun toString(): String { + val sb = StringBuilder() + when(type) { + LineType.CONTEXT -> sb.append(" ") + LineType.ADD -> sb.append("+") + LineType.DELETE -> sb.append("-") + } + sb.append(" ") + sb.append(line) + return sb.toString() + } + } + + 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) + + // 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 generatePatchedTextUsingLinks(sourceLines, patchLines) + } + + private fun generatePatchedTextUsingLinks(sourceLines: List, patchLines: List): String { + val patchedTextBuilder = StringBuilder() + val sourceLineBuffer = sourceLines.toMutableList() + + while (sourceLineBuffer.isNotEmpty()) { + // Copy all lines until the next matched line + sourceLineBuffer.takeWhile { it.matchingLine == null }.toTypedArray().forEach { + sourceLineBuffer.remove(it) + patchedTextBuilder.appendLine(it.line) + } + if(sourceLineBuffer.isEmpty()) break + val codeLine = sourceLineBuffer.removeFirst() + var patchLine = codeLine.matchingLine!! + when (patchLine.type) { + LineType.DELETE -> { /* Skip adding the line */ + } + + LineType.CONTEXT -> patchedTextBuilder.appendLine(codeLine.line) + LineType.ADD -> { + throw IllegalStateException("ADD line is matched to source line") + } + } + while (patchLine.nextLine?.type == LineType.ADD) { + patchedTextBuilder.appendLine(patchLine?.nextLine?.line) + patchLine = patchLine?.nextLine!! + } + } + + return patchedTextBuilder.toString().trimEnd() + } + + private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) { + val sourceLineMap = sourceLines.groupBy { it.line.trim() } + val patchLineMap = patchLines.filter { when(it.type) { + LineType.ADD -> false // ADD lines are not matched to source lines + else -> true + }}.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) { + 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 (patchPrev.type != LineType.ADD && 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 (patchNext.type != LineType.ADD && 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.filter { when(it.type) { + LineType.ADD -> false // ADD lines are not matched to source lines + else -> true + }}) { + 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) = setLinks(text.lines().mapIndexed { index, line -> + LineRecord(index, line) + }) + + private fun setLinks(list: List): List { + for (i in 0 until list.size) { + list[i].previousLine = if (i > 0) list[i - 1] else null + list[i].nextLine = if (i < list.size - 1) list[i + 1] else null + } + return list + } + + private fun parsePatchLines(text: String) = setLinks(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/diff/PatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/diff/PatchUtil.kt new file mode 100644 index 00000000..6ca10d24 --- /dev/null +++ b/webui/src/main/kotlin/com/github/simiacryptus/diff/PatchUtil.kt @@ -0,0 +1,3 @@ +package com.github.simiacryptus.diff + +typealias PatchUtil = IterativePatchUtil \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt index f881288c..039de070 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt @@ -87,7 +87,7 @@ class Acceptable( tabIndex: Int?, tabContent: StringBuilder, design: T, - ) = ui.hrefLink("\uD83D\uDC4D") { + ) = ui.hrefLink("Accept", classname = "href-link cmd-button") { accept(tabIndex, tabContent, design) } 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 4e7c80a5..86f81eaf 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.addApplyFileDiffLinks +import com.github.simiacryptus.diff.addApplyFileDiffLinks import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.Role @@ -249,27 +249,26 @@ class WebDevAgent( //val task = ui.newTask() return task.complete( ui.socketManager.addApplyFileDiffLinks( - root = codeFiles.keys.map { File(it).toPath() }.toTypedArray().commonRoot(), - code = codeFiles, - response = design, - handle = { newCodeMap -> - newCodeMap.forEach { (path, newCode) -> - val prev = codeFiles[path] - if (prev != newCode) { - codeFiles[path] = newCode - task.complete( - "$path Updated" - ) + root = codeFiles.keys.map { File(it).toPath() }.toTypedArray().commonRoot(), + code = codeFiles, + response = design, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + val prev = codeFiles[path] + if (prev != newCode) { + codeFiles[path] = newCode + task.complete( + "$path Updated" + ) + } } - } - }, - task = task, - ui = ui + }, + ui = ui ) ) } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt index d732f3fd..e4a60806 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt @@ -20,144 +20,145 @@ import org.eclipse.jetty.webapp.WebAppContext import java.io.File abstract class ApplicationServer( - final override val applicationName: String, - val path: String, - resourceBase: String = "application", - open val root: File = File(File(".skyenet"), applicationName), + final override val applicationName: String, + val path: String, + resourceBase: String = "application", + open val root: File = File(File(".skyenet"), applicationName), + val showMenubar: Boolean = true, ) : ChatServer(resourceBase) { - open val description: String = "" - open val singleInput = true - open val stickyInput = false - open val appInfo by lazy { - mapOf( - "applicationName" to applicationName, - "singleInput" to singleInput, - "stickyInput" to stickyInput, - "loadImages" to false, - ) - } - - final override val dataStorage: StorageInterface by lazy { dataStorageFactory(root) } - - protected open val appInfoServlet by lazy { ServletHolder("appInfo", AppInfoServlet(appInfo)) } - protected open val userInfo by lazy { ServletHolder("userInfo", UserInfoServlet()) } - protected open val usageServlet by lazy { ServletHolder("usage", UsageServlet()) } - protected open val fileZip by lazy { ServletHolder("fileZip", ZipServlet(dataStorage)) } - protected open val fileIndex by lazy { ServletHolder("fileIndex", FileServlet(dataStorage)) } - protected open val sessionSettingsServlet by lazy { ServletHolder("settings", SessionSettingsServlet(this)) } - protected open val sessionShareServlet by lazy { ServletHolder("share", SessionShareServlet(this)) } - protected open val sessionThreadsServlet by lazy { ServletHolder("threads", SessionThreadsServlet(this)) } - protected open val deleteSessionServlet by lazy { ServletHolder("delete", DeleteSessionServlet(this)) } - protected open val cancelSessionServlet by lazy { ServletHolder("cancel", CancelThreadsServlet(this)) } - - override fun newSession(user: User?, session: Session): SocketManager = - object : ApplicationSocketManager( - session = session, - owner = user, - dataStorage = dataStorage, - applicationClass = this@ApplicationServer::class.java, - ) { - override fun userMessage( + open val description: String = "" + open val singleInput = true + open val stickyInput = false + open val appInfo by lazy { + mapOf( + "applicationName" to applicationName, + "singleInput" to singleInput, + "stickyInput" to stickyInput, + "loadImages" to false, + "showMenubar" to showMenubar, + ) + } + + final override val dataStorage: StorageInterface by lazy { dataStorageFactory(root) } + + protected open val appInfoServlet by lazy { ServletHolder("appInfo", AppInfoServlet(appInfo)) } + protected open val userInfo by lazy { ServletHolder("userInfo", UserInfoServlet()) } + protected open val usageServlet by lazy { ServletHolder("usage", UsageServlet()) } + protected open val fileZip by lazy { ServletHolder("fileZip", ZipServlet(dataStorage)) } + protected open val fileIndex by lazy { ServletHolder("fileIndex", FileServlet(dataStorage)) } + protected open val sessionSettingsServlet by lazy { ServletHolder("settings", SessionSettingsServlet(this)) } + protected open val sessionShareServlet by lazy { ServletHolder("share", SessionShareServlet(this)) } + protected open val sessionThreadsServlet by lazy { ServletHolder("threads", SessionThreadsServlet(this)) } + protected open val deleteSessionServlet by lazy { ServletHolder("delete", DeleteSessionServlet(this)) } + protected open val cancelSessionServlet by lazy { ServletHolder("cancel", CancelThreadsServlet(this)) } + + override fun newSession(user: User?, session: Session): SocketManager = + object : ApplicationSocketManager( + session = session, + owner = user, + dataStorage = dataStorage, + applicationClass = this@ApplicationServer::class.java, + ) { + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + socketManager: ApplicationSocketManager, + api: API + ) = this@ApplicationServer.userMessage( + session = session, + user = user, + userMessage = userMessage, + ui = socketManager.applicationInterface, + api = api + ) + } + + open fun userMessage( session: Session, user: User?, userMessage: String, - socketManager: ApplicationSocketManager, + ui: ApplicationInterface, api: API - ) = this@ApplicationServer.userMessage( - session = session, - user = user, - userMessage = userMessage, - ui = socketManager.applicationInterface, - api = api - ) - } + ): Unit = throw UnsupportedOperationException() + + open val settingsClass: Class<*> get() = Map::class.java - open fun userMessage( - session: Session, - user: User?, - userMessage: String, - ui: ApplicationInterface, - api: API - ): Unit = throw UnsupportedOperationException() - - open val settingsClass: Class<*> get() = Map::class.java - - open fun initSettings(session: Session): T? = null - - fun getSettings( - session: Session, - userId: User?, - @Suppress("UNCHECKED_CAST") clazz: Class = settingsClass as Class - ): T? { - var settings: T? = dataStorage.getJson(userId, session, ".sys/$session/settings.json", clazz) - if (null == settings) { - val initSettings = initSettings(session) - if (null != initSettings) { - dataStorage.setJson(userId, session, ".sys/$session/settings.json", initSettings) - } - settings = dataStorage.getJson(userId, session, ".sys/$session/settings.json", clazz) + open fun initSettings(session: Session): T? = null + + fun getSettings( + session: Session, + userId: User?, + @Suppress("UNCHECKED_CAST") clazz: Class = settingsClass as Class + ): T? { + var settings: T? = dataStorage.getJson(userId, session, ".sys/$session/settings.json", clazz) + if (null == settings) { + val initSettings = initSettings(session) + if (null != initSettings) { + dataStorage.setJson(userId, session, ".sys/$session/settings.json", initSettings) + } + settings = dataStorage.getJson(userId, session, ".sys/$session/settings.json", clazz) + } + return settings } - return settings - } - - protected open fun sessionsServlet(path: String) = - ServletHolder("sessionList", SessionListServlet(this.dataStorage, path, this)) - - override fun configure(webAppContext: WebAppContext) { - super.configure(webAppContext) - - webAppContext.addFilter( - FilterHolder { request, response, chain -> - val user = authenticationManager.getUser((request as HttpServletRequest).getCookie()) - val canRead = authorizationManager.isAuthorized( - applicationClass = this@ApplicationServer.javaClass, - user = user, - operationType = OperationType.Read + + protected open fun sessionsServlet(path: String) = + ServletHolder("sessionList", SessionListServlet(this.dataStorage, path, this)) + + override fun configure(webAppContext: WebAppContext) { + super.configure(webAppContext) + + webAppContext.addFilter( + FilterHolder { request, response, chain -> + val user = authenticationManager.getUser((request as HttpServletRequest).getCookie()) + val canRead = authorizationManager.isAuthorized( + applicationClass = this@ApplicationServer.javaClass, + user = user, + operationType = OperationType.Read + ) + if (canRead) { + chain?.doFilter(request, response) + } else { + response?.writer?.write("Access Denied") + (response as HttpServletResponse?)?.status = HttpServletResponse.SC_FORBIDDEN + } + }, "/*", null ) - if (canRead) { - chain?.doFilter(request, response) - } else { - response?.writer?.write("Access Denied") - (response as HttpServletResponse?)?.status = HttpServletResponse.SC_FORBIDDEN - } - }, "/*", null - ) - - webAppContext.addServlet(appInfoServlet, "/appInfo") - webAppContext.addServlet(userInfo, "/userInfo") - webAppContext.addServlet(usageServlet, "/usage") - webAppContext.addServlet(fileIndex, "/fileIndex/*") - webAppContext.addServlet(fileZip, "/fileZip") - webAppContext.addServlet(sessionsServlet(path), "/sessions") - webAppContext.addServlet(sessionSettingsServlet, "/settings") - webAppContext.addServlet(sessionThreadsServlet, "/threads") - webAppContext.addServlet(sessionShareServlet, "/share") - webAppContext.addServlet(deleteSessionServlet, "/delete") - webAppContext.addServlet(cancelSessionServlet, "/cancel") - } - - companion object { - - fun getMimeType(filename: String): String = - when { - filename.endsWith(".html") -> "text/html" - filename.endsWith(".json") -> "application/json" - filename.endsWith(".js") -> "application/javascript" - filename.endsWith(".png") -> "image/png" - filename.endsWith(".jpg") -> "image/jpeg" - filename.endsWith(".jpeg") -> "image/jpeg" - filename.endsWith(".gif") -> "image/gif" - filename.endsWith(".svg") -> "image/svg+xml" - filename.endsWith(".css") -> "text/css" - filename.endsWith(".mp3") -> "audio/mpeg" - else -> "text/plain" - } - - fun HttpServletRequest.getCookie(name: String = AuthenticationInterface.AUTH_COOKIE) = - cookies?.find { it.name == name }?.value - - } - -} + webAppContext.addServlet(appInfoServlet, "/appInfo") + webAppContext.addServlet(userInfo, "/userInfo") + webAppContext.addServlet(usageServlet, "/usage") + webAppContext.addServlet(fileIndex, "/fileIndex/*") + webAppContext.addServlet(fileZip, "/fileZip") + webAppContext.addServlet(sessionsServlet(path), "/sessions") + webAppContext.addServlet(sessionSettingsServlet, "/settings") + webAppContext.addServlet(sessionThreadsServlet, "/threads") + webAppContext.addServlet(sessionShareServlet, "/share") + webAppContext.addServlet(deleteSessionServlet, "/delete") + webAppContext.addServlet(cancelSessionServlet, "/cancel") + } + + companion object { + + fun getMimeType(filename: String): String = + when { + filename.endsWith(".html") -> "text/html" + filename.endsWith(".json") -> "application/json" + filename.endsWith(".js") -> "application/javascript" + filename.endsWith(".png") -> "image/png" + filename.endsWith(".jpg") -> "image/jpeg" + filename.endsWith(".jpeg") -> "image/jpeg" + filename.endsWith(".gif") -> "image/gif" + filename.endsWith(".svg") -> "image/svg+xml" + filename.endsWith(".css") -> "text/css" + filename.endsWith(".mp3") -> "audio/mpeg" + else -> "text/plain" + } + + fun HttpServletRequest.getCookie(name: String = AuthenticationInterface.AUTH_COOKIE) = + cookies?.find { it.name == name }?.value + + } + +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt index 1689dfb6..5284d1b8 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt @@ -216,7 +216,7 @@ abstract class SocketManagerBase( fun hrefLink( linkText: String, - classname: String = """href-link""", + classname: String = "href-link", id: String? = null, handler: Consumer ): String { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt index 7ebfcf3f..b2bc468d 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt @@ -6,48 +6,188 @@ import com.vladsch.flexmark.ext.tables.TablesExtension import com.vladsch.flexmark.html.HtmlRenderer import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.data.MutableDataSet -import org.intellij.lang.annotations.Language +import org.apache.commons.text.StringEscapeUtils +import java.nio.file.Files object MarkdownUtil { - fun renderMarkdown( - markdown: String, - options: MutableDataSet = defaultOptions(), - tabs : Boolean = true, - ui: ApplicationInterface? = null, - ): String { - if (markdown.isBlank()) return "" - val parser = Parser.builder(options).build() - val renderer = HtmlRenderer.builder(options).build() - val document = parser.parse(markdown) - val html = renderer.render(document) - @Language("HTML") val htmlContent = html.replace( - Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL), - if(tabs) """ - |
- |
- | - | - |
- |
$1
- |
$1
- |
- |""".trimMargin() else """ - |
$1
- |""".trimMargin() - ) - //language=HTML - return if(tabs) { - displayMapInTabs(mapOf( - "HTML" to htmlContent, - "Markdown" to """
${markdown.replace(Regex("<"), "<").replace(Regex(">"), ">")}
""", - "Hide" to "", - ), ui = ui) - } else htmlContent - } + fun renderMarkdown( + markdown: String, + options: MutableDataSet = defaultOptions(), + tabs: Boolean = true, + ui: ApplicationInterface? = null, + ): String { + if (markdown.isBlank()) return "" + val parser = Parser.builder(options).build() + val renderer = HtmlRenderer.builder(options).build() + val document = parser.parse(markdown) + val html = renderer.render(document) + val mermaidRegex = + Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL) + val matches = mermaidRegex.findAll(html) + var htmlContent = html + matches.forEach { match -> + var mermaidCode = match.groups[1]!!.value + // HTML Decode mermaidCode + mermaidCode = mermaidCode + val fixedMermaidCode = fixupMermaidCode(mermaidCode) + var mermaidDiagramHTML = """
$fixedMermaidCode
""" + try { + val svg = renderMermaidToSVG(fixedMermaidCode) + if(null != ui) { + val newTask = ui.newTask(false) + newTask.complete(svg) + mermaidDiagramHTML = newTask.placeholder + } else { + mermaidDiagramHTML = svg + } + } catch (e: Exception) { + log.warn("Failed to render Mermaid diagram", e) + } + val replacement = if (tabs) """ + |
+ |
+ | + | + |
+ |
$mermaidDiagramHTML
+ |
$fixedMermaidCode
+ |
+ |""".trimMargin() else """ + |$mermaidDiagramHTML + |""".trimMargin() + htmlContent = htmlContent.replace(match.value, replacement) + } + //language=HTML + return if (tabs) { + displayMapInTabs( + mapOf( + "HTML" to htmlContent, + "Markdown" to """
${
+                        markdown.replace(Regex("<"), "<").replace(Regex(">"), ">")
+                    }
""", + "Hide" to "", + ), ui = ui + ) + } else htmlContent + } + + var MMDC_CMD: List = listOf("powershell", "mmdc") + private fun renderMermaidToSVG(mermaidCode: String): String { + // mmdc -i input.mmd -o output.svg + val tempInputFile = Files.createTempFile("mermaid", ".mmd").toFile() + val tempOutputFile = Files.createTempFile("mermaid", ".svg").toFile() + tempInputFile.writeText(StringEscapeUtils.unescapeHtml4(mermaidCode)) + val strings = MMDC_CMD + listOf("-i", tempInputFile.absolutePath, "-o", tempOutputFile.absolutePath) + val processBuilder = + ProcessBuilder(*strings.toTypedArray()) + processBuilder.redirectErrorStream(true) + val process = processBuilder.start() + val output = StringBuilder() + val errorOutput = StringBuilder() + process.inputStream.bufferedReader().use { + it.lines().forEach { line -> output.append(line) } + } + process.errorStream.bufferedReader().use { + it.lines().forEach { line -> errorOutput.append(line) } + } + process.waitFor() + val svgContent = tempOutputFile.readText() + tempInputFile.delete() + tempOutputFile.delete() + if (output.isNotEmpty()) { + log.error("Mermaid CLI Output: $output") + } + if (errorOutput.isNotEmpty()) { + log.error("Mermaid CLI Error: $errorOutput") + } + if(svgContent.isNullOrBlank()) { + throw RuntimeException("Mermaid CLI failed to generate SVG") + } + return svgContent + } + + // Simplified parsing states + enum class State { + DEFAULT, IN_NODE, IN_EDGE, IN_LABEL, IN_KEYWORD + } + fun fixupMermaidCode(code: String): String { + val stringBuilder = StringBuilder() + var index = 0 + + var currentState = State.DEFAULT + var labelStart = -1 + val keywords = listOf("graph", "subgraph", "end", "classDef", "class", "click", "style") + + while (index < code.length) { + when (currentState) { + State.DEFAULT -> { + if (code.startsWith(keywords.find { code.startsWith(it, index) } ?: "", index)) { + // Start of a keyword + currentState = State.IN_KEYWORD + stringBuilder.append(code[index]) + } else + if (code[index] == '[' || code[index] == '(' || code[index] == '{') { + // Possible start of a label + currentState = State.IN_LABEL + labelStart = index + } else if (code[index].isWhitespace() || code[index] == '-') { + // Continue in default state, possibly an edge + stringBuilder.append(code[index]) + } else { + // Start of a node + currentState = State.IN_NODE + stringBuilder.append(code[index]) + } + } + State.IN_KEYWORD -> { + if (code[index].isWhitespace()) { + // End of a keyword + currentState = State.DEFAULT + } + stringBuilder.append(code[index]) + } + State.IN_NODE -> { + if (code[index] == '-' || code[index] == '>' || code[index].isWhitespace()) { + // End of a node, start of an edge or space + currentState = if (code[index].isWhitespace()) State.DEFAULT else State.IN_EDGE + stringBuilder.append(code[index]) + } else { + // Continue in node + stringBuilder.append(code[index]) + } + } + State.IN_EDGE -> { + if (!code[index].isWhitespace() && code[index] != '-' && code[index] != '>') { + // End of an edge, start of a node + currentState = State.IN_NODE + stringBuilder.append(code[index]) + } else { + // Continue in edge + stringBuilder.append(code[index]) + } + } + State.IN_LABEL -> { + if (code[index] == ']' || code[index] == ')' || code[index] == '}') { + // End of a label + val label = code.substring(labelStart + 1, index) + val escapedLabel = "\"${label.replace("\"", "'")}\"" + stringBuilder.append(escapedLabel) + stringBuilder.append(code[index]) + currentState = State.DEFAULT + } + } + } + index++ + } + + return stringBuilder.toString() + } private fun defaultOptions(): MutableDataSet { val options = MutableDataSet() options.set(Parser.EXTENSIONS, listOf(TablesExtension.create())) return options } + + private val log = org.slf4j.LoggerFactory.getLogger(MarkdownUtil::class.java) } \ No newline at end of file diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index 5243df40..26506b25 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -36,6 +36,7 @@ let messageMap = {}; let singleInput = false; let stickyInput = false; let loadImages = "true"; +let showMenubar = true; (function () { class SvgPanZoom { @@ -188,24 +189,24 @@ function onWebSocketText(event) { messageDiv.innerHTML = messageContent; if (messagesDiv) messagesDiv.appendChild(messageDiv); substituteMessages(messageId, messageDiv); - if (singleInput) { - const mainInput = document.getElementById('main-input'); - if (mainInput) { - mainInput.style.display = 'none'; - } else { - console.log("Error: Could not find .main-input"); - } + } + if (singleInput) { + const mainInput = document.getElementById('main-input'); + if (mainInput) { + mainInput.style.display = 'none'; + } else { + console.log("Error: Could not find .main-input"); } - if (stickyInput) { - const mainInput = document.getElementById('main-input'); - if (mainInput) { - // Keep at top of screen - mainInput.style.position = 'sticky'; - mainInput.style.zIndex = '1'; - mainInput.style.top = '30px'; - } else { - console.log("Error: Could not find .main-input"); - } + } + if (stickyInput) { + const mainInput = document.getElementById('main-input'); + if (mainInput) { + // Keep at top of screen + mainInput.style.position = 'sticky'; + mainInput.style.zIndex = '1'; + mainInput.style.top = showMenubar ? '30px' : '0px'; + } else { + console.log("Error: Could not find .main-input"); } } if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; @@ -499,6 +500,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!response.ok) { console.error('There was a problem with the fetch operation:', error); } + return response.json(); }) .then(data => { if (data) { @@ -514,6 +516,25 @@ document.addEventListener('DOMContentLoaded', () => { if (data.loadImages) { loadImages = data.loadImages; } + if (data.showMenubar != null) { + showMenubar = data.showMenubar; + if (data.showMenubar === false) { + const menubar = document.getElementById('toolbar'); + if (menubar) menubar.style.display = 'none'; + const namebar = document.getElementById('namebar'); + if (namebar) namebar.style.display = 'none'; + const mainInput = document.getElementById('main-input'); + if (mainInput) { + mainInput.style.top = '0px'; + } + const session = document.getElementById('session'); + if (session) { + session.style.top = '0px'; + session.style.width = '100%'; + session.style.position = 'absolute'; + } + } + } } }) .catch(error => { @@ -578,5 +599,4 @@ document.addEventListener('DOMContentLoaded', () => { tosLink.addEventListener('click', () => showModal('/tos.html', false)); } }) -; - +; \ No newline at end of file diff --git a/webui/src/main/resources/shared/_main.scss b/webui/src/main/resources/shared/_main.scss index 82c670d4..f5440dba 100644 --- a/webui/src/main/resources/shared/_main.scss +++ b/webui/src/main/resources/shared/_main.scss @@ -1,390 +1,428 @@ // Mixins @mixin typography($font-family: $font-family-primary, $font-size: $font-size-base, $font-weight: $font-weight-normal) { - font-family: $font-family; - font-size: $font-size; - font-weight: $font-weight; + font-family: $font-family; + font-size: $font-size; + font-weight: $font-weight; } @mixin flex-container($direction: column) { - display: flex; - flex-direction: $direction; + display: flex; + flex-direction: $direction; } @mixin fixed-full { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100%; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100%; } @mixin link-hover-transition { - transition: color $transition-speed; - &:hover { - color: $link-hover-color; - } + transition: color $transition-speed; + &:hover { + color: $link-hover-color; + } } @mixin message-style { - padding: 10px; - margin-bottom: 10px; - border-radius: $border-radius; + padding: 10px; + margin-bottom: 10px; + border-radius: $border-radius; } // Root styles body { - @include typography($font-family-secondary); - color: $primary-text-color; - background-color: $primary-bg-color; - margin: 0; - padding: 30px 0 50px; + @include typography($font-family-secondary); + color: $primary-text-color; + background-color: $primary-bg-color; + margin: 0; + padding: 30px 0 50px; } #messages { - @include flex-container; - padding: 10px; - background-color: $secondary-bg-color; - box-shadow: $box-shadow; + @include flex-container; + padding: 10px; + background-color: $secondary-bg-color; + box-shadow: $box-shadow; } .chat-input, .reply-input { - background-color: $secondary-bg-color; - color: $primary-text-color; - border-radius: $border-radius; - padding: 10px; - margin-bottom: 10px; - overflow: auto; - resize: vertical; - flex: 1; - border: 1px solid $border-color; - box-shadow: $box-shadow; + background-color: $secondary-bg-color; + color: $primary-text-color; + border-radius: $border-radius; + padding: 10px; + margin-bottom: 10px; + overflow: auto; + resize: vertical; + flex: 1; + border: 1px solid $border-color; + box-shadow: $box-shadow; } #main-input, .reply-form, .code-execution { - @include flex-container(row); - padding: 5px; - width: -webkit-fill-available; - background-color: $primary-bg-color; + @include flex-container(row); + padding: 5px; + width: -webkit-fill-available; + background-color: $primary-bg-color; } .href-link { - text-decoration: none; - color: $link-color; - @include link-hover-transition; + text-decoration: none; + color: $link-color; + @include link-hover-transition; } #disconnected-overlay { - @include fixed-full; - display: none; - background-color: $modal-overlay-color; - z-index: 50; - @include flex-container; - color: white; - font-size: $font-size-large; - p { - @include typography($font-size: $font-size-large, $font-weight: $font-weight-bold); - line-height: 1.5; - margin-bottom: 20px; - animation: bounce $transition-speed infinite alternate; - position: relative; - color: firebrick; - } + @include fixed-full; + display: none; + background-color: $modal-overlay-color; + z-index: 50; + @include flex-container; + color: white; + font-size: $font-size-large; + + p { + @include typography($font-size: $font-size-large, $font-weight: $font-weight-bold); + line-height: 1.5; + margin-bottom: 20px; + animation: bounce $transition-speed infinite alternate; + position: relative; + color: firebrick; + } } .spinner-border { - display: block; - width: 40px; - height: 40px; - border: 4px solid $spinner-border-color; - border-left-color: $link-color; - border-radius: 50%; - animation: spin 1s linear infinite; + display: block; + width: 40px; + height: 40px; + border: 4px solid $spinner-border-color; + border-left-color: $link-color; + border-radius: 50%; + animation: spin 1s linear infinite; } .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } #toolbar, #namebar { - background-color: $toolbar-bg-color; + background-color: $toolbar-bg-color; + padding: 5px; + position: fixed; + top: 0; + text-align: left; + box-shadow: $box-shadow; + + a { + color: $toolbar-text-color; + text-decoration: none; padding: 5px; - position: fixed; - top: 0; - text-align: left; - box-shadow: $box-shadow; - a { - color: $toolbar-text-color; - text-decoration: none; - padding: 5px; - @include link-hover-transition; - &:hover { - background-color: $toolbar-hover-bg-color; - color: $toolbar-hover-text-color; - } + @include link-hover-transition; + + &:hover { + background-color: $toolbar-hover-bg-color; + color: $toolbar-hover-text-color; } + } } #toolbar { - width: 100vw; - z-index: 2; + width: 100vw; + z-index: 2; } #namebar { - z-index: 3; - right: 0; + z-index: 3; + right: 0; } .modal { - @include fixed-full; - display: none; - z-index: 100; - overflow: auto; - background-color: $modal-overlay-color; - .modal-content { - background-color: $secondary-bg-color; - margin: 10% auto; - padding: 30px; - border: 1px solid $border-color; - width: 60%; - position: relative; - border-radius: $border-radius; - box-shadow: $box-shadow; - } - .close { - @include typography($font-weight: $font-weight-bold); - color: $close-color; - float: right; - font-size: $font-size-large; - cursor: pointer; - &:hover, - &:focus { - color: $close-hover-color; - } + @include fixed-full; + display: none; + z-index: 100; + overflow: auto; + background-color: $modal-overlay-color; + + .modal-content { + background-color: $secondary-bg-color; + margin: 10% auto; + padding: 30px; + border: 1px solid $border-color; + width: 60%; + position: relative; + border-radius: $border-radius; + box-shadow: $box-shadow; + } + + .close { + @include typography($font-weight: $font-weight-bold); + color: $close-color; + float: right; + font-size: $font-size-large; + cursor: pointer; + + &:hover, + &:focus { + color: $close-hover-color; } + } } .close-button { - margin-left: 95%; + margin-left: 95%; } .play-button, .regen-button, .cancel-button, .close-button { - @include typography($font-size: 1.5rem, $font-weight: $font-weight-bold); - border: 2px solid transparent; - background: $primary-bg-color; - cursor: pointer; - transition: all $transition-speed; - padding: 5px 10px; - border-radius: $border-radius; - text-decoration: unset; - &:focus, - &:hover { - outline: none; - background-color: darken($primary-bg-color, 5%); - border-color: $link-color; - } - &:active { - transform: scale(0.95); - } + @include typography($font-size: 1.5rem, $font-weight: $font-weight-bold); + border: 2px solid transparent; + background: $primary-bg-color; + cursor: pointer; + transition: all $transition-speed; + padding: 5px 10px; + border-radius: $border-radius; + text-decoration: unset; + + &:focus, + &:hover { + outline: none; + background-color: darken($primary-bg-color, 5%); + border-color: $link-color; + } + + &:active { + transform: scale(0.95); + } } .cancel-button { - right: 0; - position: absolute; + right: 0; + position: absolute; } .error { - color: $error-color; + color: $error-color; } .verbose { - display: block; + display: block; } .verbose-hidden { - display: none; + display: none; } .user-message, .response-message, .reply-message { - @include message-style; + @include message-style; } .user-message { - background-color: $user-message-bg; - border: 1px solid $user-message-border; + background-color: $user-message-bg; + border: 1px solid $user-message-border; } .reply-message { - background-color: $reply-message-bg; - border: 1px solid $reply-message-border; - display: flex; + background-color: $reply-message-bg; + border: 1px solid $reply-message-border; + display: flex; } .response-message { - background-color: $response-message-bg; - border: 1px solid $response-message-border; - display: block; + background-color: $response-message-bg; + border: 1px solid $response-message-border; + display: block; } pre.verbose, pre.response-message { - background-color: $verbose-pre-bg; - border: 1px solid $verbose-pre-border; - border-radius: $border-radius; - padding: 15px; - overflow-x: auto; - font-family: 'Courier New', Courier, monospace; + background-color: $verbose-pre-bg; + border: 1px solid $verbose-pre-border; + border-radius: $border-radius; + padding: 15px; + overflow-x: auto; + font-family: 'Courier New', Courier, monospace; } .response-header { - font-weight: $font-weight-bold; - margin-top: 20px; - margin-bottom: 10px; + font-weight: $font-weight-bold; + margin-top: 20px; + margin-bottom: 10px; } #footer { - position: fixed; - bottom: 0; - width: 100vw; - text-align: right; - z-index: 1000; - a { - color: $footer-link-color; - text-decoration: none; - font-weight: $font-weight-bold; - &:hover { - text-decoration: underline; - color: $footer-link-hover-color; - } + position: fixed; + bottom: 0; + width: 100vw; + text-align: right; + z-index: 1000; + + a { + color: $footer-link-color; + text-decoration: none; + font-weight: $font-weight-bold; + + &:hover { + text-decoration: underline; + color: $footer-link-hover-color; } + } } .dropdown { - position: relative; - display: inline-block; - &:hover { - .dropdown-content { - display: block; - } - .dropbtn { - background-color: $dropdown-btn-bg; - } - } + position: relative; + display: inline-block; + + &:hover { .dropdown-content { - display: none; - position: absolute; - background-color: $dropdown-content-bg; - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1; - a { - color: $primary-text-color; - padding: 12px 16px; - text-decoration: none; - display: block; - &:hover { - background-color: $dropdown-content-hover-bg; - } - } + display: block; } + .dropbtn { - border: none; - cursor: pointer; + background-color: $dropdown-btn-bg; + } + } + + .dropdown-content { + display: none; + position: absolute; + background-color: $dropdown-content-bg; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + + a { + color: $primary-text-color; + padding: 12px 16px; + text-decoration: none; + display: block; + + &:hover { + background-color: $dropdown-content-hover-bg; + } } + } + + .dropbtn { + border: none; + cursor: pointer; + } } .applist { - border-collapse: collapse; - margin-top: 20px; - th { - padding-top: 15px; - padding-bottom: 15px; - background-color: $applist-header-bg; - color: $applist-header-text; - text-transform: uppercase; - font-weight: $font-weight-bold; - } - td, th { - border: 1px solid #ddd; - padding: 8px; - text-align: left; - } - tr { - &:nth-child(even) { - background-color: $applist-row-even-bg; - } - &:hover { - background-color: $applist-row-hover-bg; - } - } - th { - padding-top: 12px; - padding-bottom: 12px; - background-color: $applist-header-bg; - color: $applist-header-text; + border-collapse: collapse; + margin-top: 20px; + + th { + padding-top: 15px; + padding-bottom: 15px; + background-color: $applist-header-bg; + color: $applist-header-text; + text-transform: uppercase; + font-weight: $font-weight-bold; + } + + td, th { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + } + + tr { + &:nth-child(even) { + background-color: $applist-row-even-bg; } - a { - color: $applist-link-color; - text-decoration: none; - margin-right: 10px; + + &:hover { + background-color: $applist-row-hover-bg; } + } + + th { + padding-top: 12px; + padding-bottom: 12px; + background-color: $applist-header-bg; + color: $applist-header-text; + } + + a { + color: $applist-link-color; + text-decoration: none; + margin-right: 10px; + } } .new-session-link { - background-color: $new-session-link-bg; - color: white; - padding: 5px 10px; - border-radius: 5px; - display: inline-block; - margin-right: 0; - &:hover { - background-color: $new-session-link-hover-bg; - } + background-color: $new-session-link-bg; + color: white; + padding: 5px 10px; + border-radius: 5px; + display: inline-block; + margin-right: 0; + + &:hover { + background-color: $new-session-link-hover-bg; + } } // Keyframes @keyframes bounce { - 0% { - transform: translateY(0); - } - 100% { - transform: translateY(-10px); - } + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-10px); + } } @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .tab-content { - display: none; + display: none; } .tab-content.active { - display: block; + display: block; } .tab-button.active { - background-color: $button-bg-color; + background-color: $button-bg-color; +} + +.cmd-button { + background-color: $button-bg-color; + color: $button-text-color; + padding: 10px 15px; + border-radius: $border-radius; + border: none; + cursor: pointer; + transition: background-color $transition-speed; + + &:hover { + background-color: darken($button-bg-color, 10%); + color: lighten($button-text-color, 10%); + } } diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/diff/ApxPatchUtilTest.kt similarity index 82% rename from webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt rename to webui/src/test/kotlin/com/github/simiacryptus/diff/ApxPatchUtilTest.kt index 971cd095..901913bd 100644 --- a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt +++ b/webui/src/test/kotlin/com/github/simiacryptus/diff/ApxPatchUtilTest.kt @@ -1,6 +1,6 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test internal class ApxPatchUtilTest { @@ -18,7 +18,7 @@ internal class ApxPatchUtilTest { """.trimIndent() val expected = "Added Line\nLine 1\nLine 2\nLine 3" val result = ApxPatchUtil.patch(source, patch) - assertEquals(expected, result) + Assertions.assertEquals(expected, result) } @Test @@ -33,6 +33,6 @@ internal class ApxPatchUtilTest { """.trimIndent() val expected = "Line 1\nLine 3" val result = ApxPatchUtil.patch(source, patch) - assertEquals(expected, result) + Assertions.assertEquals(expected, result) } } \ No newline at end of file diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/diff/DiffUtilTest.kt similarity index 86% rename from webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffUtilTest.kt rename to webui/src/test/kotlin/com/github/simiacryptus/diff/DiffUtilTest.kt index 048052ec..b0c87fd4 100644 --- a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffUtilTest.kt +++ b/webui/src/test/kotlin/com/github/simiacryptus/diff/DiffUtilTest.kt @@ -1,6 +1,6 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class DiffUtilTest { @@ -27,7 +27,7 @@ class DiffUtilTest { line2 + line3 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent an addition.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent an addition.") } // @Test @@ -41,7 +41,7 @@ class DiffUtilTest { - line2 line3 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent a deletion.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent a deletion.") } @Test @@ -56,7 +56,7 @@ class DiffUtilTest { + line3 line4 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent mixed changes.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent mixed changes.") } // @Test @@ -71,7 +71,7 @@ class DiffUtilTest { + changed_line2 line3 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly include context lines.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly include context lines.") } @Test @@ -85,7 +85,7 @@ class DiffUtilTest { + changed_line1 line2 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent changes at the start.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent changes at the start.") } @Test @@ -99,7 +99,7 @@ class DiffUtilTest { - line2 + changed_line2 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent changes at the end.") + Assertions.assertEquals(expectedDiff, formattedDiff, "The diff should correctly represent changes at the end.") } // @Test @@ -114,7 +114,11 @@ class DiffUtilTest { + changed_line2 line3 """.trimIndent() - assertEquals(expectedDiff, formattedDiff, "The diff should correctly handle cases with no context lines.") + Assertions.assertEquals( + expectedDiff, + formattedDiff, + "The diff should correctly handle cases with no context lines." + ) } @Test @@ -183,4 +187,4 @@ class DiffUtilTest { println("\n\nEcho Patch:\n\n") DiffUtil.formatDiff(patchLines).lines().forEach { println(it) } } -} +} \ 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/diff/IterativePatchUtilTest.kt similarity index 88% rename from webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt rename to webui/src/test/kotlin/com/github/simiacryptus/diff/IterativePatchUtilTest.kt index f4cc732b..7e4e2463 100644 --- a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt +++ b/webui/src/test/kotlin/com/github/simiacryptus/diff/IterativePatchUtilTest.kt @@ -1,6 +1,6 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.diff -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class IterativePatchUtilTest { @@ -18,7 +18,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.patch(source, patch) - assertEquals(source.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + Assertions.assertEquals(source.replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -41,7 +41,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.patch(source, patch) - assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + Assertions.assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -63,7 +63,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.patch(source, patch) - assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + Assertions.assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test @@ -75,6 +75,7 @@ class IterativePatchUtilTest { """.trimIndent() val patch = """ line1 + - line2 line3 """.trimIndent() val expected = """ @@ -82,7 +83,7 @@ class IterativePatchUtilTest { line3 """.trimIndent() val result = IterativePatchUtil.patch(source, patch) - assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) + Assertions.assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n")) } @Test fun testFromData() { @@ -145,8 +146,9 @@ class IterativePatchUtilTest { } """.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(), " ")) + Assertions.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 diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtilTest.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtilTest.kt new file mode 100644 index 00000000..3fe67d80 --- /dev/null +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtilTest.kt @@ -0,0 +1,34 @@ +package com.simiacryptus.skyenet.webui.util + +import com.vladsch.flexmark.util.data.MutableDataSet +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class MarkdownUtilTest { + + private val markdownUtil = MarkdownUtil + + @Test + fun testRenderMarkdown_BlankInput() { + val result = markdownUtil.renderMarkdown("") + assertEquals("", result, "Rendering blank markdown should return an empty string.") + } + + @Test + fun testRenderMarkdown_SimpleMarkdown() { + val markdown = "# Heading\n\nThis is a test." + val expectedHtml = "

Heading

\n

This is a test.

\n" + val result = markdownUtil.renderMarkdown(markdown, MutableDataSet(), false, null) + assert(result.contains(expectedHtml)) { "Rendered HTML should contain the expected HTML content." } + } + + @Test + fun testFixupMermaidCode() { + val input = "graph TD; A-->B; A[Square Rect] -- Link text --> C(Round Rect); C --> D{Rhombus};" + val expected = "graph TD; A-->B; A[Square Rect] -- Link text --> C(Round Rect); C --> D{Rhombus};" + val result = markdownUtil.fixupMermaidCode(input) + assertEquals(expected, result, "The Mermaid code should be correctly formatted.") + } + + +} \ No newline at end of file