Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved HTML export in tests #706

Merged
merged 1 commit into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -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 <reified E> toHtml(
result: SuiteResults<E>,
htmlFilename: String = "index.html"
) where E : AI.PromptClassifier, E : Enum<E> {
val content = Json.encodeToString(SuiteResults.serializer(serializer<E>()), 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}")
}
}
}
Expand Down
@@ -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 """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Tests</title>
<style>$cssContent</style>
<script>
$jsContent
const testData = $contentJson;
</script>
</head>
<body>
<div id="test-container"></div>
</body>
</html>
"""
.trimIndent()
}
}
}