From e65667d921d0b0d64170298b9f7b493b9df70697 Mon Sep 17 00:00:00 2001 From: Javi Pacheco Date: Wed, 27 Mar 2024 13:42:50 +0100 Subject: [PATCH] Improved HTML export in tests --- .../functional/xef/evaluator/SuiteBuilder.kt | 44 +--- .../functional/xef/evaluator/output/Html.kt | 193 ++++++++++++++++++ 2 files changed, 204 insertions(+), 33 deletions(-) create mode 100644 evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/output/Html.kt diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt index 02ad5e9e2..ecf90f62d 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt @@ -2,17 +2,17 @@ package com.xebia.functional.xef.evaluator import com.xebia.functional.openai.models.CreateChatCompletionRequestModel import com.xebia.functional.xef.AI -import com.xebia.functional.xef.evaluator.errors.FileNotFound import com.xebia.functional.xef.evaluator.models.ItemResult import com.xebia.functional.xef.evaluator.models.OutputResponse import com.xebia.functional.xef.evaluator.models.OutputResult import com.xebia.functional.xef.evaluator.models.SuiteResults +import com.xebia.functional.xef.evaluator.output.Html import java.io.File import kotlin.jvm.JvmSynthetic import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer class SuiteBuilder( private val description: String, @@ -57,7 +57,6 @@ data class SuiteSpec( ItemResult(item.input, outputResults) } val suiteResults = SuiteResults(description, model.value, E::class.simpleName, items) - export(Json.encodeToString(suiteResults)) return suiteResults } @@ -68,39 +67,18 @@ data class SuiteSpec( model: CreateChatCompletionRequestModel, block: suspend SuiteBuilder.() -> Unit ): SuiteSpec = SuiteBuilder(description, model).apply { block() }.build() - } - - fun export(content: String): Boolean { - return arrow.core.raise.recover({ - // Read the content of `index.html` inside resources folder - val indexHTML = - SuiteSpec::class.java.getResource("/web/index.html")?.readText() - ?: raise(FileNotFound("index.html")) - val scriptJS = - SuiteSpec::class.java.getResource("/web/script.js")?.readText() - ?: raise(FileNotFound("script.js")) - val styleCSS = - SuiteSpec::class.java.getResource("/web/style.css")?.readText() - ?: raise(FileNotFound("style.css")) - val contentJS = "const testData = $content;" - // Copy all the files inside build folder + inline fun toHtml( + result: SuiteResults, + htmlFilename: String = "index.html" + ) where E : AI.PromptClassifier, E : Enum { + val content = Json.encodeToString(SuiteResults.serializer(serializer()), result) + // Copy file inside build folder val outputPath = System.getProperty("user.dir") + "/build/testSuite" File(outputPath).mkdirs() - File("$outputPath/index.html").writeText(indexHTML) - File("$outputPath/script.js").writeText(scriptJS) - File("$outputPath/style.css").writeText(styleCSS) - File("$outputPath/content.js").writeText(contentJS) - val url = File("$outputPath/index.html").toURI() - println("Test suite exported to $url") - true - }) { - when (it) { - else -> { - println(it.message("File not found")) - false - } - } + val htmlFile = File("$outputPath/$htmlFilename") + htmlFile.writeText(Html.get(content)) + println("Test suite exported to ${htmlFile.absoluteFile}") } } } diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/output/Html.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/output/Html.kt new file mode 100644 index 000000000..f206c40c3 --- /dev/null +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/output/Html.kt @@ -0,0 +1,193 @@ +package com.xebia.functional.xef.evaluator.output + +class Html { + + companion object { + + // language=javascript + private val jsContent = + """ + document.addEventListener('DOMContentLoaded', function() { + const container = document.getElementById('test-container'); + + const headerDiv = document.createElement('div'); + headerDiv.classList.add('test-block'); + + const header = document.createElement('h1'); + header.classList.add('test-header'); + header.textContent = "Suite test"; + + const suiteDescription = document.createElement('p'); + suiteDescription.textContent = 'Description: ' + testData.description; + + const model = document.createElement('p'); + model.textContent = 'Model: ' + testData.model; + + const metric = document.createElement('p'); + metric.textContent = 'Metric: ' + testData.metric; + + headerDiv.appendChild(header); + headerDiv.appendChild(suiteDescription); + headerDiv.appendChild(model); + headerDiv.appendChild(metric); + + container.appendChild(headerDiv); + + testData.items.forEach(block => { + const blockDiv = document.createElement('div'); + blockDiv.classList.add('test-block'); + + const title = document.createElement('h2'); + title.classList.add('test-title'); + title.textContent = 'Input: ' + block.description; + + blockDiv.appendChild(title); + + block.items.forEach(test => { + const itemDescription = document.createElement('div'); + itemDescription.textContent = 'Description: ' + test.description; + blockDiv.appendChild(itemDescription); + + const context = document.createElement('div'); + context.textContent = 'Context: ' + test.contextDescription; + blockDiv.appendChild(context); + + const outputDiv = document.createElement('pre'); + outputDiv.classList.add('output'); + outputDiv.innerText = 'Output: ' + test.output; + outputDiv.addEventListener('click', function() { + this.classList.toggle('expanded'); + }); + blockDiv.appendChild(outputDiv); + + const result = document.createElement('div'); + result.classList.add('score', test.success ? 'score-passed' : 'score-failed'); + result.textContent = 'Result: ' + test.result; + blockDiv.appendChild(result); + + blockDiv.appendChild(document.createElement('br')); + }); + container.appendChild(blockDiv); + }); + + }); + """ + .trimIndent() + + // language=css + private val cssContent = + """ + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; + } + + #test-container { + width: 80%; + margin: 20px auto; + padding: 15px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .test-block { + margin-bottom: 20px; + border-bottom: 1px solid #eee; + padding-bottom: 20px; + } + + .test-title { + font-size: 1.2em; + color: #333; + } + + .input, .output { + margin: 5px 0; + } + + .input-passed { + margin-top: 25px; + color: green; + font-weight: bold; + } + + .input-failed { + margin-top: 25px; + color: red; + font-weight: bold; + } + + .output { + color: #666; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .output.expanded { + white-space: normal; + } + + .score { + font-weight: bold; + } + + .score-passed { + margin-bottom: 25px; + color: #008000; + } + + .score-failed { + margin-bottom: 25px; + color: red; + } + + .avg-score, .test-info { + font-size: 1.2em; + color: #d35400; + margin-top: 10px; + } + + .test-summary { + background-color: #e7e7e7; + padding: 15px; + margin-top: 20px; + border-radius: 8px; + } + + .test-summary h3 { + font-size: 1.1em; + color: #555; + margin-top: 0; + } + + """ + .trimIndent() + + fun get(contentJson: String): String { + // language=html + return """ + + + + + Tests + + + + +
+ + + """ + .trimIndent() + } + } +}