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) """
- |
- |
- | Diagram
- | Source
- |
- |
- |
- |
- |""".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) """
+ |
+ |
+ | Diagram
+ | Source
+ |
+ |
$mermaidDiagramHTML
+ |
+ |
+ |""".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 \nThis 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