diff --git a/build.gradle.kts b/build.gradle.kts index 25cc52c..e0bf46f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin + kotlin("plugin.serialization") version "1.9.24" } group = properties("pluginGroup").get() @@ -21,6 +22,7 @@ repositories { } dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation(libs.flexmark) } diff --git a/src/main/kotlin/com/github/fmueller/jarvis/conversation/Conversation.kt b/src/main/kotlin/com/github/fmueller/jarvis/conversation/Conversation.kt new file mode 100644 index 0000000..8c9507b --- /dev/null +++ b/src/main/kotlin/com/github/fmueller/jarvis/conversation/Conversation.kt @@ -0,0 +1,29 @@ +package com.github.fmueller.jarvis.conversation + +import java.beans.PropertyChangeListener +import java.beans.PropertyChangeSupport +import java.time.LocalDateTime + +enum class Role { + ASSISTANT, USER +} + +data class Message(val role: Role, val content: String, val createdAt: LocalDateTime = LocalDateTime.now()) + +class Conversation { + + private val messages = mutableListOf() + private val propertyChangeSupport = PropertyChangeSupport(this) + + fun addMessage(message: Message) { + val oldMessages = ArrayList(messages) + messages.add(message) + propertyChangeSupport.firePropertyChange("message", oldMessages, ArrayList(messages)) + } + + fun getMessages() = messages.toList() + + fun addPropertyChangeListener(listener: PropertyChangeListener) { + propertyChangeSupport.addPropertyChangeListener(listener) + } +} diff --git a/src/main/kotlin/com/github/fmueller/jarvis/services/OllamaService.kt b/src/main/kotlin/com/github/fmueller/jarvis/services/OllamaService.kt index aafe590..6e70e38 100644 --- a/src/main/kotlin/com/github/fmueller/jarvis/services/OllamaService.kt +++ b/src/main/kotlin/com/github/fmueller/jarvis/services/OllamaService.kt @@ -5,12 +5,21 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.thisLogger import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.util.* +@Serializable +private data class Response(val message: Message) + +@Serializable +private data class Message(val role: String, val content: String) + @Service(Service.Level.PROJECT) class OllamaService : Disposable { @@ -66,10 +75,10 @@ class OllamaService : Disposable { ) .build() - val response = client.send(request, HttpResponse.BodyHandlers.ofString()) - val body = response.body() - thisLogger().warn("Response: $body") - body + val httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()) + val json = Json { ignoreUnknownKeys = true } + val response = json.decodeFromString(httpResponse.body()) + response.message.content } catch (e: Exception) { "Error: ${e.message}" } diff --git a/src/main/kotlin/com/github/fmueller/jarvis/toolWindow/ConversationWindowFactory.kt b/src/main/kotlin/com/github/fmueller/jarvis/toolWindow/ConversationWindowFactory.kt index 2db0f23..1ecbef1 100644 --- a/src/main/kotlin/com/github/fmueller/jarvis/toolWindow/ConversationWindowFactory.kt +++ b/src/main/kotlin/com/github/fmueller/jarvis/toolWindow/ConversationWindowFactory.kt @@ -1,5 +1,8 @@ package com.github.fmueller.jarvis.toolWindow +import com.github.fmueller.jarvis.conversation.Conversation +import com.github.fmueller.jarvis.conversation.Message +import com.github.fmueller.jarvis.conversation.Role import com.github.fmueller.jarvis.services.OllamaService import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -21,10 +24,7 @@ import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.awt.event.KeyAdapter import java.awt.event.KeyEvent -import javax.swing.BorderFactory -import javax.swing.JEditorPane -import javax.swing.JPanel -import javax.swing.UIManager +import javax.swing.* import javax.swing.border.AbstractBorder class ConversationWindowFactory : ToolWindowFactory { @@ -41,21 +41,33 @@ class ConversationWindowFactory : ToolWindowFactory { private val project = toolWindow.project private val ollama = project.service() + private val conversation = Conversation() - fun getContent() = BorderLayoutPanel().apply { - - val conversationPanel = JEditorPane().apply { - editorKit = HTMLEditorKitBuilder.simple() - text = markdownToHtml( - """ + private val conversationPanel = JEditorPane().apply { + editorKit = HTMLEditorKitBuilder.simple() + text = markdownToHtml( + """ ## Hello! I am Jarvis, your personal coding assistant. I try to be helpful, but I am not perfect. """.trimIndent() - ) - isEditable = false + ) + isEditable = false + } + + init { + conversation.addPropertyChangeListener { + if (it.propertyName == "message") { + val messages = conversation.getMessages() + SwingUtilities.invokeLater { + conversationPanel.text = + markdownToHtml(messages.joinToString("\n") { "**" + it.role.toString() + "**\n" + it.content }) + } + } } + } + fun getContent() = BorderLayoutPanel().apply { var borderColor = JBColor.GRAY val inputArea = JBTextArea().apply { lineWrap = true @@ -90,8 +102,12 @@ class ConversationWindowFactory : ToolWindowFactory { override fun keyPressed(e: KeyEvent) { if (e.keyCode == KeyEvent.VK_ENTER) { val question = inputArea.text + conversation.addMessage(Message(Role.USER, question)) + inputArea.text = "" - conversationPanel.text = markdownToHtml(ollama.ask(question)) + + val answer = ollama.ask(question) + conversation.addMessage(Message(Role.ASSISTANT, answer)) } } }) @@ -112,7 +128,7 @@ class ConversationWindowFactory : ToolWindowFactory { Parser.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create()) ) - options.set(HtmlRenderer.SOFT_BREAK, "
\n") + options.set(HtmlRenderer.SOFT_BREAK, "
") val parser: Parser = Parser.builder(options).build() HtmlRenderer.builder(options).build().render(parser.parse(text)) }