AiChatFenceCatalog.kt
src/jvmMain/kotlin/afield/ai/conversation/AiChatFenceCatalog.kt · 69 lines
package afield.ai.conversation
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
The fenced blocks AI4 chat draws as pictures rather than printing as code, read on the JVM from the same catalog the browser renderers read.
src/commonMain/resources/ai4-fence-renderers.json holds one entry per language, each naming the
renderer module and export that draws it in the chat panel alongside the syntax guidance and the
worked example the model is shown. There are two today, mermaid and math. The JVM reads that
file out of its resources and turns it into the paragraph of fence instructions added to the
chat system prompt, so a language the model is told about always has a renderer behind it and a
language the browser cannot draw is never offered. Only chat mode gets that paragraph; the other
instruction modes describe no fences.
The catalog is checked on first read for a known schema version, one entry per language, a lowercase language name and no blank field. A malformed or absent catalog stops the read and says what is wrong with it, instead of leaving the panel one renderer short of what it advertised.
internal object AiChatFenceCatalog
The same packaged catalog that registers preview renderers in the browser.
data class Entry
@Serializable
data class Entry(
val language: String,
val name: String,
val purpose: String,
val syntaxGuidance: String,
val example: String,
val modulePath: String,
val exportName: String,
)
@Serializable
private data class FileFormat(val version: Int, val fences: List<Entry>)
val entries: List<Entry> by lazy
val entries: List<Entry> by lazy {
val text = checkNotNull(javaClass.getResourceAsStream("/ai4-fence-renderers.json")) {
"AI4 fence catalog is missing from JVM resources"
}.bufferedReader(Charsets.UTF_8).use { it.readText() }
val file = Json.decodeFromString<FileFormat>(text)
require(file.version == 1) { "Unsupported AI4 fence catalog version ${file.version}" }
require(file.fences.map { it.language }.distinct().size == file.fences.size) {
"Duplicate AI4 fence language"
}
file.fences.forEach { entry ->
require(Regex("[a-z][a-z0-9_+-]*").matches(entry.language)) { "Invalid fence language" }
require(listOf(entry.name, entry.purpose, entry.syntaxGuidance, entry.example,
entry.modulePath, entry.exportName).all { it.isNotBlank() }) { "Incomplete fence ${entry.language}" }
}
file.fences
}
fun instructionText(fences: List<Entry> = entries): String
fun instructionText(fences: List<Entry> = entries): String = buildString {
if (fences.isEmpty()) return@buildString
appendLine("AI4 chat renders these complete fenced blocks. Use one when it clarifies the answer; ordinary code fences remain code:")
for (entry in fences) {
appendLine("- `${entry.language}` (${entry.name}): ${entry.purpose}")
appendLine(" Syntax: ${entry.syntaxGuidance}")
appendLine(" Example:")
appendLine(" ```${entry.language}")
entry.example.lineSequence().forEach { appendLine(" $it") }
appendLine(" ```")
}
}.trimEnd()
}