diff --git a/README.md b/README.md index 66091f06..5d95c256 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.43 + 1.0.44 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.43' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.44' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.43") +implementation("com.simiacryptus:skyenet:1.0.44") ``` ### 🌟 To Use diff --git a/core/src/main/java/com/simiacryptus/skyenet/core/OutputInterceptor.java b/core/src/main/java/com/simiacryptus/skyenet/core/OutputInterceptor.java index 1dac9ad2..52590b81 100644 --- a/core/src/main/java/com/simiacryptus/skyenet/core/OutputInterceptor.java +++ b/core/src/main/java/com/simiacryptus/skyenet/core/OutputInterceptor.java @@ -7,7 +7,7 @@ import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; -public class OutputInterceptor { +public final class OutputInterceptor { private OutputInterceptor() { // Prevent instantiation of the utility class diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt index f870ddf2..245ecc28 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt @@ -36,7 +36,7 @@ open class CodingActor( get() = interpreterClass.java.getConstructor(Map::class.java).newInstance(symbols + runtimeSymbols) data class CodeRequest( - val messages: List, + val messages: List>, val codePrefix: String = "", val autoEvaluate: Boolean = false, val fixIterations: Int = 4, @@ -99,8 +99,8 @@ open class CodingActor( ), ) + questions.messages.map { ChatMessage( - role = Role.user, - content = it.toContentList() + role = it.second, + content = it.first.toContentList() ) } if (questions.codePrefix.isNotBlank()) { @@ -170,8 +170,8 @@ open class CodingActor( } log.info("Result: $result") //language=HTML - val executionResult = ExecutionResult(result.toString(), OutputInterceptor.getGlobalOutput()) - OutputInterceptor.clearGlobalOutput() + val executionResult = ExecutionResult(result.toString(), OutputInterceptor.getThreadOutput()) + OutputInterceptor.clearThreadOutput() return executionResult } @@ -280,22 +280,22 @@ open class CodingActor( ChatMessage( Role.assistant, """ - |```${language.lowercase()} - |${previousCode} - |``` - |""".trimMargin().trim().toContentList() + |```${language.lowercase()} + |${previousCode} + |``` + |""".trimMargin().trim().toContentList() ), ChatMessage( Role.system, """ - |The previous code failed with the following error: - | - |``` - |${error.message?.trim() ?: ""} - |``` - | - |Correct the code and try again. - |""".trimMargin().trim().toContentList() + |The previous code failed with the following error: + | + |``` + |${error.message?.trim() ?: ""} + |``` + | + |Correct the code and try again. + |""".trimMargin().trim().toContentList() ) ) ) diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt index e7745986..b7c414d4 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt @@ -171,6 +171,7 @@ interface UsageInterface { fun getUserUsageSummary(user: User): Map fun getSessionUsageSummary(session: Session): Map + fun clear() data class UsageKey( val session: Session, diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/UsageManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/UsageManager.kt index 60adf12f..b9b3916b 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/UsageManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/UsageManager.kt @@ -12,165 +12,187 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -open class UsageManager(val root : File = File(".skyenet/usage")) : UsageInterface { +open class UsageManager(val root: File = File(".skyenet/usage")) : UsageInterface { - private val scheduler = Executors.newSingleThreadScheduledExecutor() - private val txLogFile = File(root, "log.csv") - @Volatile private var txLogFileWriter: FileWriter? - private val usagePerSession = ConcurrentHashMap() - private val sessionsByUser = ConcurrentHashMap>() - private val usersBySession = ConcurrentHashMap>() + private val scheduler = Executors.newSingleThreadScheduledExecutor() + private val txLogFile = File(root, "log.csv") + @Volatile + private var txLogFileWriter: FileWriter? + private val usagePerSession = ConcurrentHashMap() + private val sessionsByUser = ConcurrentHashMap>() + private val usersBySession = ConcurrentHashMap>() - init { - txLogFile.parentFile.mkdirs() - loadFromLog(txLogFile) - txLogFileWriter = FileWriter(txLogFile, true) - scheduler.scheduleAtFixedRate({ saveCounters() }, 1, 1, TimeUnit.HOURS) - } + init { + txLogFile.parentFile.mkdirs() + loadFromLog(txLogFile) + txLogFileWriter = FileWriter(txLogFile, true) + scheduler.scheduleAtFixedRate({ saveCounters() }, 1, 1, TimeUnit.HOURS) + } - @Suppress("MemberVisibilityCanBePrivate") - private fun loadFromLog(file: File) { - if (file.exists()) { - try { - file.readLines().forEach { line -> - val (sessionId, user, model, value, direction) = line.split(",") - val modelEnum = listOf( - ChatModels.values(), - CompletionModels.values(), - EditModels.values(), - EmbeddingModels.values() - ).flatMap { it.toList() }.find { model == it.modelName } - ?: throw RuntimeException("Unknown model $model") - when (direction) { - "input" -> incrementUsage( - Session(sessionId), - User(email=user), - modelEnum, - com.simiacryptus.jopenai.ApiModel.Usage(prompt_tokens = value.toInt()) - ) + @Suppress("MemberVisibilityCanBePrivate") + private fun loadFromLog(file: File) { + if (file.exists()) { + try { + file.readLines().forEach { line -> + val (sessionId, user, model, value, direction) = line.split(",") + val modelEnum = listOf( + ChatModels.values(), + CompletionModels.values(), + EditModels.values(), + EmbeddingModels.values() + ).flatMap { it.toList() }.find { model == it.modelName } + ?: throw RuntimeException("Unknown model $model") + when (direction) { + "input" -> incrementUsage( + Session(sessionId), + User(email = user), + modelEnum, + com.simiacryptus.jopenai.ApiModel.Usage(prompt_tokens = value.toInt()) + ) - "output" -> incrementUsage( - Session(sessionId), - User(email=user), - modelEnum, - com.simiacryptus.jopenai.ApiModel.Usage(completion_tokens = value.toInt()) - ) + "output" -> incrementUsage( + Session(sessionId), + User(email = user), + modelEnum, + com.simiacryptus.jopenai.ApiModel.Usage(completion_tokens = value.toInt()) + ) - "cost" -> incrementUsage( - Session(sessionId), - User(email=user), - modelEnum, - com.simiacryptus.jopenai.ApiModel.Usage(cost = value.toDouble()) - ) + "cost" -> incrementUsage( + Session(sessionId), + User(email = user), + modelEnum, + com.simiacryptus.jopenai.ApiModel.Usage(cost = value.toDouble()) + ) - else -> throw RuntimeException("Unknown direction $direction") - } - } - } catch (e: Exception) { - log.warn("Error loading log file", e) - } + else -> throw RuntimeException("Unknown direction $direction") + } } + } catch (e: Exception) { + log.warn("Error loading log file", e) + } } + } - @Suppress("MemberVisibilityCanBePrivate") - private fun writeCompactLog(file: File) { - FileWriter(file).use { writer -> - usagePerSession.forEach { (sessionId, usage) -> - val user = usersBySession[sessionId]?.firstOrNull() - usage.tokensPerModel.forEach { (model, counter) -> - writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.inputTokens.get()},input\n") - writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.outputTokens.get()},output\n") - writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.cost.get()},cost\n") - } - } - writer.flush() + @Suppress("MemberVisibilityCanBePrivate") + private fun writeCompactLog(file: File) { + FileWriter(file).use { writer -> + usagePerSession.forEach { (sessionId, usage) -> + val user = usersBySession[sessionId]?.firstOrNull() + usage.tokensPerModel.forEach { (model, counter) -> + writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.inputTokens.get()},input\n") + writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.outputTokens.get()},output\n") + writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.cost.get()},cost\n") } + } + writer.flush() } + } - private fun saveCounters() { - txLogFileWriter = FileWriter(txLogFile, true) - val timedFile = File(txLogFile.absolutePath + "." + System.currentTimeMillis()) - writeCompactLog(timedFile) - val swapFile = File(txLogFile.absolutePath + ".old") - synchronized(txLogFile) { - try { - txLogFileWriter?.close() - } catch (e: Exception) { - log.warn("Error closing log file", e) - } - try { - txLogFile.renameTo(swapFile) - } catch (e: Exception) { - log.warn("Error renaming log file", e) - } - try { - timedFile.renameTo(txLogFile) - } catch (e: Exception) { - log.warn("Error renaming log file", e) - } - try { - swapFile.renameTo(timedFile) - } catch (e: Exception) { - log.warn("Error renaming log file", e) - } - txLogFileWriter = FileWriter(txLogFile, true) - } - File(root,"counters.json").writeText(JsonUtil.toJson(usagePerSession)) + private fun saveCounters() { + txLogFileWriter = FileWriter(txLogFile, true) + val timedFile = File(txLogFile.absolutePath + "." + System.currentTimeMillis()) + writeCompactLog(timedFile) + val swapFile = File(txLogFile.absolutePath + ".old") + synchronized(txLogFile) { + try { + txLogFileWriter?.close() + } catch (e: Exception) { + log.warn("Error closing log file", e) + } + try { + txLogFile.renameTo(swapFile) + } catch (e: Exception) { + log.warn("Error renaming log file", e) + } + try { + timedFile.renameTo(txLogFile) + } catch (e: Exception) { + log.warn("Error renaming log file", e) + } + try { + swapFile.renameTo(timedFile) + } catch (e: Exception) { + log.warn("Error renaming log file", e) + } + txLogFileWriter = FileWriter(txLogFile, true) } + val text = JsonUtil.toJson(usagePerSession) + File(root, "counters.json").writeText(text) + val toClean = txLogFile.parentFile.listFiles() + ?.filter { it.name.startsWith(txLogFile.name) && it.name != txLogFile.absolutePath } + ?.sortedBy { it.lastModified() } // oldest first + ?.dropLast(2) // keep 2 newest + ?.drop(2) // keep 2 oldest + toClean?.forEach { it.delete() } + } - override fun incrementUsage(session: Session, user: User?, model: OpenAIModel, tokens: com.simiacryptus.jopenai.ApiModel.Usage) { - @Suppress("NAME_SHADOWING") val user = if (null == user) null else User(email = user.email) // Hack - usagePerSession.computeIfAbsent(session) { UsageCounters() } - .tokensPerModel.computeIfAbsent(UsageKey(session, user, model)) { UsageValues() } - .addAndGet(tokens) - if (user != null) { - sessionsByUser.computeIfAbsent(user) { HashSet() }.add(session) - } - try { - val txLogFileWriter = txLogFileWriter - if (null != txLogFileWriter) { - synchronized(txLogFile) { - txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.prompt_tokens},input\n") - txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.completion_tokens},output\n") - txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.completion_tokens},cost\n") - txLogFileWriter.flush() - } - } - } catch (e: Exception) { - log.warn("Error incrementing usage", e) + override fun incrementUsage( + session: Session, + user: User?, + model: OpenAIModel, + tokens: com.simiacryptus.jopenai.ApiModel.Usage + ) { + @Suppress("NAME_SHADOWING") val user = if (null == user) null else User(email = user.email) // Hack + usagePerSession.computeIfAbsent(session) { UsageCounters() } + .tokensPerModel.computeIfAbsent(UsageKey(session, user, model)) { UsageValues() } + .addAndGet(tokens) + if (user != null) { + sessionsByUser.computeIfAbsent(user) { HashSet() }.add(session) + } + try { + val txLogFileWriter = txLogFileWriter + if (null != txLogFileWriter) { + synchronized(txLogFile) { + txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.prompt_tokens},input\n") + txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.completion_tokens},output\n") + txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.completion_tokens},cost\n") + txLogFileWriter.flush() } + } + } catch (e: Exception) { + log.warn("Error incrementing usage", e) } + } - override fun getUserUsageSummary(user: User): Map { - @Suppress("NAME_SHADOWING") val user = if(null == user) null else User(email= user.email) // Hack - return sessionsByUser[user]?.flatMap { sessionId -> - val usage = usagePerSession[sessionId] - usage?.tokensPerModel?.entries?.map { (model, counter) -> - model.model to counter.toUsage() - } ?: emptyList() - }?.groupBy { it.first }?.mapValues { - it.value.map { it.second }.reduce { a, b -> - com.simiacryptus.jopenai.ApiModel.Usage( - prompt_tokens = a.prompt_tokens + b.prompt_tokens, - completion_tokens = a.completion_tokens + b.completion_tokens, - cost = (a.cost ?: 0.0) + (b.cost ?: 0.0) - ) - } - } ?: emptyMap() - } + override fun getUserUsageSummary(user: User): Map { + @Suppress("NAME_SHADOWING") val user = if (null == user) null else User(email = user.email) // Hack + return sessionsByUser[user]?.flatMap { sessionId -> + val usage = usagePerSession[sessionId] + usage?.tokensPerModel?.entries?.map { (model, counter) -> + model.model to counter.toUsage() + } ?: emptyList() + }?.groupBy { it.first }?.mapValues { + it.value.map { it.second }.reduce { a, b -> + com.simiacryptus.jopenai.ApiModel.Usage( + prompt_tokens = a.prompt_tokens + b.prompt_tokens, + completion_tokens = a.completion_tokens + b.completion_tokens, + cost = (a.cost ?: 0.0) + (b.cost ?: 0.0) + ) + } + } ?: emptyMap() + } - override fun getSessionUsageSummary(session: Session): Map = - usagePerSession[session]?.tokensPerModel?.entries?.map { (model, counter) -> - model.model to counter.toUsage() - }?.groupBy { it.first }?.mapValues { it.value.map { it.second }.reduce { a, b -> - com.simiacryptus.jopenai.ApiModel.Usage( - prompt_tokens = a.prompt_tokens + b.prompt_tokens, - completion_tokens = a.completion_tokens + b.completion_tokens, - cost = (a.cost ?: 0.0) + (b.cost ?: 0.0) - ) - } } ?: emptyMap() + override fun getSessionUsageSummary(session: Session): Map = + usagePerSession[session]?.tokensPerModel?.entries?.map { (model, counter) -> + model.model to counter.toUsage() + }?.groupBy { it.first }?.mapValues { + it.value.map { it.second }.reduce { a, b -> + com.simiacryptus.jopenai.ApiModel.Usage( + prompt_tokens = a.prompt_tokens + b.prompt_tokens, + completion_tokens = a.completion_tokens + b.completion_tokens, + cost = (a.cost ?: 0.0) + (b.cost ?: 0.0) + ) + } + } ?: emptyMap() - companion object { - private val log = org.slf4j.LoggerFactory.getLogger(UsageManager::class.java) - } + override fun clear() { + usagePerSession.clear() + sessionsByUser.clear() + usersBySession.clear() + saveCounters() + } + + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(UsageManager::class.java) + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 8c337a03..ae7ae0ee 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.43 +libraryVersion = 1.0.44 gradleVersion = 7.6.1 diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt index f6740ef4..8e675efd 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt @@ -1,6 +1,7 @@ package com.simiacryptus.skyenet.apps.coding import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.proxy.ValidatedObject import com.simiacryptus.skyenet.core.Interpreter import com.simiacryptus.skyenet.core.actors.ActorSystem @@ -27,8 +28,9 @@ open class CodingAgent( val interpreter: KClass, val symbols: Map, temperature: Double = 0.1, + val details: String? = null, val actorMap: Map = mapOf( - ActorTypes.CodingActor to CodingActor(interpreter, symbols = symbols, temperature = temperature) + ActorTypes.CodingActor to CodingActor(interpreter, symbols = symbols, temperature = temperature, details = details) ), ) : ActorSystem(actorMap, dataStorage, user, session) { enum class ActorTypes { @@ -53,7 +55,7 @@ open class CodingAgent( val message = ui.newTask() try { message.echo(renderMarkdown(userMessage)) - val codeRequest = CodingActor.CodeRequest(listOf(userMessage)) + val codeRequest = CodingActor.CodeRequest(listOf(userMessage to ApiModel.Role.user)) displayCode(message, codeRequest) } catch (e: Throwable) { log.warn("Error", e) @@ -90,15 +92,20 @@ open class CodingAgent( response: CodeResult ) { var formHandle: StringBuilder? = null - var playLink: StringBuilder? = null - playLink = task.add(if (!canPlay) "" else { - ui.hrefLink("▶", "href-link play-button") { - formHandle?.clear() - playLink?.clear() - val header = task.header("Running...") - try { - val result = response.result - val feedback = """ + val playHandler: (t: Unit) -> Unit = { + formHandle?.clear() + val header = task.header("Running...") + try { + val result = response.result + val feedback = when { + result.resultValue.isBlank() || result.resultValue.trim().lowercase() == "null" -> """ + |# Output + |```text + |${result.resultOutput} + |``` + """.trimMargin() + + else -> """ |# Result |``` |${result.resultValue} @@ -108,64 +115,75 @@ open class CodingAgent( |```text |${result.resultOutput} |``` - """.trimMargin() - header?.clear() - task.add(renderMarkdown(feedback)) - displayFeedback(task, revise(request, response, feedback), response) - } catch (e: Throwable) { - header?.clear() - val message = when { - e is ValidatedObject.ValidationError -> renderMarkdown(e.message ?: "") - e is CodingActor.FailedToImplementException -> renderMarkdown( - """ - |**Failed to Implement** - | - |${e.message} - | - |""".trimMargin() - ) + """.trimMargin() + } + header?.clear() + task.add(renderMarkdown(feedback)) + displayFeedback(task, CodingActor.CodeRequest( + messages = request.messages + + listOf( + response.code to ApiModel.Role.assistant, + feedback to ApiModel.Role.system, + ).filter { it.first.isNotBlank() } + ), response) + } catch (e: Throwable) { + header?.clear() + val message = when { + e is ValidatedObject.ValidationError -> renderMarkdown(e.message ?: "") + e is CodingActor.FailedToImplementException -> renderMarkdown( + """ + |**Failed to Implement** + | + |${e.message} + | + |""".trimMargin() + ) - else -> renderMarkdown( - """ - |**Error `${e.javaClass.name}`** - | - |```text - |${e.message} - |``` - |""".trimMargin() - ) - } - task.add(message, true, "div", "error") - displayCode(task, revise(request, response, message)) + else -> renderMarkdown( + """ + |**Error `${e.javaClass.name}`** + | + |```text + |${e.message} + |``` + |""".trimMargin() + ) } + task.add(message, true, "div", "error") + displayCode(task, CodingActor.CodeRequest( + messages = request.messages + + listOf( + response.code to ApiModel.Role.assistant, + message to ApiModel.Role.system, + ).filter { it.first.isNotBlank() } + )) } - }) - formHandle = task.add(ui.textInput { feedback -> + } + val feedbackHandler: (t: String) -> Unit = { feedback -> try { formHandle?.clear() - playLink?.clear() task.echo(renderMarkdown(feedback)) - displayCode(task, revise(request, response, feedback)) + displayCode(task, CodingActor.CodeRequest( + messages = request.messages + + listOf( + response?.code to ApiModel.Role.assistant, + feedback to ApiModel.Role.user, + ).filter { it.first?.isNotBlank() == true }.map { it.first!! to it.second } + )) } catch (e: Throwable) { log.warn("Error", e) task.error(e) } - }) + } + formHandle = task.add( + """ + |${if (canPlay) ui.hrefLink("▶", "href-link play-button", playHandler) else ""} + |${ui.textInput(feedbackHandler)} + """.trimMargin(), className = "reply-message" + ) task.complete() } - open fun revise( - request: CodingActor.CodeRequest, - response: CodeResult?, - feedback: String - ) = CodingActor.CodeRequest( - messages = request.messages + - listOf( - response?.code, - feedback - ).filterNotNull().filter { it.isNotBlank() } - ) - companion object { private val log = LoggerFactory.getLogger(CodingAgent::class.java) } 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 b1025c73..6ac0b5da 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 @@ -28,10 +28,12 @@ abstract class ApplicationServer( open val description: String = "" open val singleInput = true + open val stickyInput = false open val appInfo: Any by lazy { mapOf( "applicationName" to applicationName, - "singleInput" to singleInput + "singleInput" to singleInput, + "stickyInput" to stickyInput ) } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt index 13257c70..e06d80f8 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt @@ -16,8 +16,8 @@ open class ChatSocketManager( session: Session, val model: OpenAITextModel = ChatModels.GPT35Turbo, val userInterfacePrompt: String, - private val initialAssistantPrompt: String = "", - val systemPrompt: String, + open val initialAssistantPrompt: String = "", + open val systemPrompt: String, val api: OpenAIClient, val temperature: Double = 0.3, applicationClass: Class, diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt index 48477467..103c3fc5 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt @@ -1,6 +1,7 @@ package com.simiacryptus.skyenet.webui.test import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.skyenet.core.actors.CodingActor import com.simiacryptus.skyenet.core.platform.ApplicationServices import com.simiacryptus.skyenet.core.platform.AuthorizationInterface.OperationType @@ -29,7 +30,7 @@ open class CodingActorTestApp( val message = ui.newTask() try { message.echo(renderMarkdown(userMessage)) - val response = actor.answer(CodingActor.CodeRequest(listOf(userMessage)), api = api) + val response = actor.answer(CodingActor.CodeRequest(listOf(userMessage to ApiModel.Role.user)), api = api) val canPlay = ApplicationServices.authorizationManager.isAuthorized( this::class.java, user, diff --git a/webui/src/main/resources/application/main.css b/webui/src/main/resources/application/main.css index 559866f1..eaf62e3b 100644 --- a/webui/src/main/resources/application/main.css +++ b/webui/src/main/resources/application/main.css @@ -60,7 +60,7 @@ body { display: flex; flex-direction: column; height: 100vh; - padding-top: 40px; + padding-top: 20px; } .reply-form { @@ -69,7 +69,7 @@ body { left: 0; right: 0; top: 0; - display: flex; + display: contents; margin: 0; padding: 5px; flex-shrink: 0; @@ -282,7 +282,7 @@ pre { } } -.user-message, .response-message { +.user-message, .response-message .reply-message { padding: 10px; margin-bottom: 10px; border-radius: 4px; @@ -293,9 +293,16 @@ pre { border: 1px solid #d0eaff; } +.reply-message { + background-color: #fff; + border: 1px solid #eee; + display: flex; +} + .response-message { background-color: #fff; border: 1px solid #eee; + display: block; } pre.verbose, pre.response-message { diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index 66e0ba1d..2933a0d7 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -27,6 +27,7 @@ async function fetchData(endpoint, useSession = true) { let messageVersions = {}; let singleInput = false; +let stickyInput = false; function onWebSocketText(event) { console.log('WebSocket message:', event); @@ -61,11 +62,23 @@ function onWebSocketText(event) { 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"); + } + } } messagesDiv.scrollTop = messagesDiv.scrollHeight; Prism.highlightAll(); refreshVerbose(); + refreshReplyForms() } function toggleVerbose() { @@ -87,6 +100,25 @@ function toggleVerbose() { } } +function refreshReplyForms() { + document.querySelectorAll('.reply-input').forEach(messageInput => { + messageInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + let form = messageInput.closest('form'); + if (form) { + let textSubmitButton = form.querySelector('.text-submit-button'); + if (textSubmitButton) { + textSubmitButton.click(); + } else { + form.dispatchEvent(new Event('submit', { cancelable: true })); + } + } + } + }); + }); +} + function refreshVerbose() { let verboseToggle = document.getElementById('verbose'); @@ -221,6 +253,9 @@ document.addEventListener('DOMContentLoaded', () => { if (data.singleInput) { singleInput = data.singleInput; } + if (data.stickyInput) { + stickyInput = data.stickyInput; + } }) .catch(error => { console.error('There was a problem with the fetch operation:', error);