diff --git a/src/main/kotlin/ai/codemaker/jetbrains/window/AssistantWindowFactory.kt b/src/main/kotlin/ai/codemaker/jetbrains/window/AssistantWindowFactory.kt index 6f31e71..6e961d0 100644 --- a/src/main/kotlin/ai/codemaker/jetbrains/window/AssistantWindowFactory.kt +++ b/src/main/kotlin/ai/codemaker/jetbrains/window/AssistantWindowFactory.kt @@ -9,7 +9,6 @@ import ai.codemaker.jetbrains.assistant.Role import ai.codemaker.jetbrains.file.FileExtensions import ai.codemaker.jetbrains.service.CodeMakerService import ai.codemaker.jetbrains.settings.AppSettingsState -import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager @@ -25,8 +24,6 @@ import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.ui.jcef.JBCefBrowserBuilder import com.intellij.ui.jcef.JBCefJSQuery import com.intellij.util.ui.JBUI -import org.cef.browser.CefBrowser -import org.cef.handler.CefLoadHandlerAdapter import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser @@ -42,7 +39,7 @@ import javax.swing.JTextField class AssistantWindowFactory : ToolWindowFactory, DumbAware { object AssistantWindowFactory { - const val ASSISTANT_VIEW = "/webview/assistant.html" + const val ASSISTANT_HOME_VIEW = "file:///assistant.html" } override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { @@ -74,8 +71,11 @@ class AssistantWindowFactory : ToolWindowFactory, DumbAware { private fun createChatPanel(): Component { chatScreen.setProperty(JBCefBrowserBase.Properties.NO_CONTEXT_MENU, true) - chatScreen.jbCefClient.addLoadHandler(LoadHandler(), chatScreen.cefBrowser) - chatScreen.loadHTML(assistantView()) + chatScreen.setOpenLinksInExternalBrowser(true) + chatScreen.loadURL(AssistantWindowFactory.ASSISTANT_HOME_VIEW) + val resourceHandler = FileResourceProvider() + resourceHandler.addResource("/") { StreamResourceHandler("webview", this) } + chatScreen.jbCefClient.addRequestHandler(resourceHandler, chatScreen.cefBrowser) return chatScreen.component } @@ -120,8 +120,10 @@ class AssistantWindowFactory : ToolWindowFactory, DumbAware { if (AppSettingsState.instance.apiKey.isNullOrEmpty()) { addMessage( - "To use Assistant features, please first set the API Key in the Extension Settings." + - "\nYou can create free account [here](https://portal.codemaker.ai/#/register).", Role.Assistant) + "To use Assistant features, please first set the API Key in the Extension Settings." + + "\nYou can create free account [here](https://portal.codemaker.ai/#/register).", + Role.Assistant + ) return } @@ -156,39 +158,12 @@ class AssistantWindowFactory : ToolWindowFactory, DumbAware { chatScreen.cefBrowser.executeJavaScript("window.appendMessage(\"$content\", ${assistant})", "", 0) } - private fun assistantView(): String { - return AssistantWindowFactory::class.java - .getResource(AssistantWindowFactory.ASSISTANT_VIEW)!! - .readText() - } - private fun renderMarkdown(source: String): String { val flavour = CommonMarkFlavourDescriptor() val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(source) return HtmlGenerator(source, parsedTree, flavour).generateHtml() } - private fun registerEventHandler() { - chatScreen.cefBrowser.executeJavaScript( - "window.openLink = function(link) { " + jsQuery.inject("link") + "};", - chatScreen.cefBrowser.url, 0) - - chatScreen.cefBrowser.executeJavaScript(""" - document.addEventListener('click', function(e) { - e.preventDefault(); - const link = e.target.closest('a').href; - if (link) { - window.openLink(link); - } - }); - """.trimIndent(), chatScreen.cefBrowser.url, 0) - - jsQuery.addHandler { - BrowserUtil.browse(it) - return@addHandler null - } - } - inner class MessageTextKeyListener : KeyListener { override fun keyTyped(e: KeyEvent?) { } @@ -202,11 +177,5 @@ class AssistantWindowFactory : ToolWindowFactory, DumbAware { override fun keyReleased(e: KeyEvent?) { } } - - inner class LoadHandler : CefLoadHandlerAdapter() { - override fun onLoadingStateChange(browser: CefBrowser?, isLoading: Boolean, canGoBack: Boolean, canGoForward: Boolean) { - registerEventHandler() - } - } } } \ No newline at end of file diff --git a/src/main/kotlin/ai/codemaker/jetbrains/window/FileResourceProvider.kt b/src/main/kotlin/ai/codemaker/jetbrains/window/FileResourceProvider.kt new file mode 100644 index 0000000..45dda91 --- /dev/null +++ b/src/main/kotlin/ai/codemaker/jetbrains/window/FileResourceProvider.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 CodeMaker AI Inc. All rights reserved. + */ + +package ai.codemaker.jetbrains.window + +import com.intellij.openapi.util.io.toNioPath +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.callback.CefCallback +import org.cef.handler.* +import org.cef.misc.BoolRef +import org.cef.network.CefRequest +import java.net.URL + +private typealias CefResourceProvider = () -> CefResourceHandler? + +class FileResourceProvider() : CefRequestHandlerAdapter() { + + private val protocol = "file" + + private val resources = HashMap() + + private val REJECTING_RESOURCE_HANDLER: CefResourceHandler = object : CefResourceHandlerAdapter() { + override fun processRequest(request: CefRequest, callback: CefCallback): Boolean { + callback.cancel() + return false + } + } + + private val RESOURCE_REQUEST_HANDLER = object : CefResourceRequestHandlerAdapter() { + override fun getResourceHandler( + browser: CefBrowser?, + frame: CefFrame?, + request: CefRequest + ): CefResourceHandler? { + val url = URL(request.url) + if (protocol != url.protocol) { + return REJECTING_RESOURCE_HANDLER + } + + return resolveHandler(url) + } + } + + fun addResource(path: String, resourceProvider: CefResourceProvider) { + resources[path] = resourceProvider + } + + override fun getResourceRequestHandler( + browser: CefBrowser?, + frame: CefFrame?, + request: CefRequest?, + isNavigation: Boolean, + isDownload: Boolean, + requestInitiator: String?, + disableDefaultHandling: BoolRef? + ): CefResourceRequestHandler { + return RESOURCE_REQUEST_HANDLER + } + + private fun resolveHandler(url: URL): CefResourceHandler? { + var path = url.path.toNioPath() + while (path != null) { + val handler = resources[path.toString()] + if (handler != null) { + return handler() + } + path = path.parent + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/ai/codemaker/jetbrains/window/StreamResourceHandler.kt b/src/main/kotlin/ai/codemaker/jetbrains/window/StreamResourceHandler.kt new file mode 100644 index 0000000..606db3f --- /dev/null +++ b/src/main/kotlin/ai/codemaker/jetbrains/window/StreamResourceHandler.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2023 CodeMaker AI Inc. All rights reserved. + */ + +package ai.codemaker.jetbrains.window + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPath +import io.ktor.util.* +import org.cef.callback.CefCallback +import org.cef.handler.CefResourceHandler +import org.cef.misc.IntRef +import org.cef.misc.StringRef +import org.cef.network.CefRequest +import org.cef.network.CefResponse +import java.io.IOException +import java.io.InputStream +import java.net.URL +import java.nio.file.Path + +class StreamResourceHandler(private val resourcePath: String, parent: Disposable) : CefResourceHandler, Disposable { + + private var input: InputStream? = null + private var mimeType = "text/html" + + init { + Disposer.register(parent, this) + } + + override fun processRequest(request: CefRequest?, callback: CefCallback?): Boolean { + val url = URL(request!!.url) + val path = url.path + + input = this.javaClass.classLoader.getResourceAsStream(Path.of(resourcePath, path).toString()) + if (input == null) { + return false + } + + when (path.toNioPath().extension) { + "html" -> mimeType = "text/html"; + "svg" -> mimeType = "image/svg+xml"; + } + + callback!!.Continue() + return true + } + + override fun getResponseHeaders(response: CefResponse?, responseLength: IntRef?, redirectUrl: StringRef?) { + responseLength!!.set(input!!.available()) + response!!.mimeType = mimeType + response!!.status = 200 + } + + override fun readResponse( + dataOut: ByteArray?, + bytesToRead: Int, + bytesRead: IntRef?, + callback: CefCallback? + ): Boolean { + try { + bytesRead!!.set(input!!.read(dataOut!!, 0, bytesToRead)) + if (bytesRead!!.get() != -1) { + return true + } + } catch (e: IOException) { + callback!!.cancel() + } + bytesRead!!.set(0) + Disposer.dispose(this) + return false + } + + override fun cancel() { + Disposer.dispose(this) + } + + override fun dispose() { + try { + input?.close() + } catch (e: IOException) { + // ignores + } + } +} \ No newline at end of file diff --git a/src/main/resources/webview/assistant.html b/src/main/resources/webview/assistant.html index d473fe3..5892ad1 100644 --- a/src/main/resources/webview/assistant.html +++ b/src/main/resources/webview/assistant.html @@ -71,6 +71,12 @@ animation-delay: -0.16s; } + img.icon { + width: 12px; + height: 12px; + margin: 4px; + } + @keyframes shine { 0%, 80%, 100% { transform: scale(0); @@ -106,6 +112,25 @@ message.innerHTML = body; card.appendChild(message); + const controls = document.createElement("div"); + controls.classList.add("controls"); + card.append(controls); + + const upVoteButton = document.createElement('img'); + upVoteButton.classList.add('icon'); + upVoteButton.src = "media/thumbs-up-off.svg"; + controls.append(upVoteButton); + + const downVoteButton = document.createElement('img'); + downVoteButton.classList.add('icon'); + downVoteButton.src = "media/thumbs-down-off.svg"; + controls.append(downVoteButton); + + const copyButton = document.createElement('img'); + copyButton.classList.add('icon'); + copyButton.src = "media/copy-off.svg"; + controls.append(copyButton); + document.getElementById("chat").appendChild(card); document.getElementById("anchor").scrollIntoView({behavior: "smooth"}); } diff --git a/src/main/resources/webview/media/copy-off.svg b/src/main/resources/webview/media/copy-off.svg new file mode 100644 index 0000000..2b56ed6 --- /dev/null +++ b/src/main/resources/webview/media/copy-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/webview/media/copy.svg b/src/main/resources/webview/media/copy.svg new file mode 100644 index 0000000..0d76d48 --- /dev/null +++ b/src/main/resources/webview/media/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/webview/media/thumbs-down-off.svg b/src/main/resources/webview/media/thumbs-down-off.svg new file mode 100644 index 0000000..3f58a34 --- /dev/null +++ b/src/main/resources/webview/media/thumbs-down-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/webview/media/thumbs-down.svg b/src/main/resources/webview/media/thumbs-down.svg new file mode 100644 index 0000000..4b1fd83 --- /dev/null +++ b/src/main/resources/webview/media/thumbs-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/webview/media/thumbs-up-off.svg b/src/main/resources/webview/media/thumbs-up-off.svg new file mode 100644 index 0000000..d6168dd --- /dev/null +++ b/src/main/resources/webview/media/thumbs-up-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/webview/media/thumbs-up.svg b/src/main/resources/webview/media/thumbs-up.svg new file mode 100644 index 0000000..7da882b --- /dev/null +++ b/src/main/resources/webview/media/thumbs-up.svg @@ -0,0 +1 @@ + \ No newline at end of file