From 63572de3ebbeb99ddbb7155c4b4a3ad2708f3b2c Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 12 Nov 2023 13:22:20 -0500 Subject: [PATCH] 1.2.18 (#105) * Fixing errors on startup for sanctioned systems * 1.2.18 * Update gradle.properties * 1.2.18 * Update settings.gradle.kts * 1.2.18 * 1.2.18 * wip * wip * wip * wip * wip * Update build.gradle.kts * wip * Update build.gradle.kts * 1.2.18 --- .github/workflows/build.yml | 2 +- .gitignore | 5 +- CHANGELOG.md | 7 + build.gradle.kts | 34 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 6 +- gradlew | 33 +- gradlew.bat | 1 + .../actions/code/GenerateProjectAction.groovy | 29 +- .../actions/generic/AnalogueFileAction.groovy | 1 - .../actions/generic/CreateFileAction.groovy | 1 - .../generic/GenerateStoryAction.groovy | 16 +- .../simiacryptus/aicoder/ApplicationEvents.kt | 54 -- .../aicoder/actions/FileContextAction.kt | 21 +- .../aicoder/actions/SelectionAction.kt | 30 +- .../aicoder/actions/dev/AppServer.kt | 12 +- .../aicoder/actions/dev/CodeChatAction.kt | 10 +- .../aicoder/actions/dev/CodeChatServer.kt | 99 +-- .../aicoder/actions/dev/ConvertFileTo.kt | 16 +- .../actions/dev/LaunchSkyenetAction.kt | 54 -- .../aicoder/actions/dev/PrintTreeAction.kt | 5 +- .../dev/SkyenetProjectCodingSessionServer.kt | 64 -- .../actions/generic/DictationAction.kt | 13 +- .../aicoder/actions/generic/RedoLast.kt | 5 +- .../actions/markdown/MarkdownListAction.kt | 14 +- .../aicoder/config/ActionSettingsRegistry.kt | 20 +- .../aicoder/config/ActionTable.kt | 12 +- .../aicoder/config/AppSettingsComponent.kt | 8 +- .../aicoder/config/AppSettingsConfigurable.kt | 3 +- .../aicoder/config/AppSettingsState.kt | 14 +- .../simiacryptus/aicoder/config/MRUItems.kt | 4 +- .../aicoder/ui/ModelSelectionWidgetFactory.kt | 11 +- .../ui/TemperatureControlWidgetFactory.kt | 28 +- .../aicoder/ui/TokenCountWidgetFactory.kt | 7 +- .../simiacryptus/aicoder/util/BlockComment.kt | 8 +- .../aicoder/util/ComputerLanguage.kt | 12 +- .../aicoder/util/IdeaOpenAIClient.kt | 15 +- .../simiacryptus/aicoder/util/LineComment.kt | 12 +- .../simiacryptus/aicoder/util/UITools.kt | 185 +---- .../aicoder/util/psi/PsiClassContext.kt | 5 +- .../aicoder/util/psi/PsiTranslationTree.kt | 12 +- .../simiacryptus/aicoder/util/psi/PsiUtil.kt | 53 +- .../skyenet/heart/WeakGroovyInterpreter.kt | 1 + .../skyenet/heart/WeakKotlinInterpreter.kt | 1 + src/main/resources/META-INF/plugin.xml | 14 +- src/main/resources/codeChat/chat.css | 278 +++++++ src/main/resources/codeChat/chat.js | 65 ++ src/main/resources/codeChat/favicon.svg | 724 ++++++++++++++++++ src/main/resources/codeChat/index.html | 37 + src/main/resources/codeChat/main.js | 128 ++++ .../actions/code/GenerateProjectAction.groovy | 29 +- .../actions/generic/AnalogueFileAction.groovy | 1 - .../actions/generic/CreateFileAction.groovy | 1 - .../generic/GenerateStoryAction.groovy | 16 +- .../aicoder/ApplicationDevelopmentUITest.kt | 123 +-- .../com/github/simiacryptus/aicoder/DocGen.kt | 13 - .../com/github/simiacryptus/aicoder/UITest.kt | 1 - .../github/simiacryptus/aicoder/UITestUtil.kt | 149 +--- .../aicoder/actions/ActionTestBase.kt | 8 - .../aicoder/util/IdeaOpenAIClientTest.kt | 16 - 61 files changed, 1528 insertions(+), 1020 deletions(-) delete mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/ApplicationEvents.kt delete mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt delete mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt create mode 100644 src/main/resources/codeChat/chat.css create mode 100644 src/main/resources/codeChat/chat.js create mode 100644 src/main/resources/codeChat/favicon.svg create mode 100644 src/main/resources/codeChat/index.html create mode 100644 src/main/resources/codeChat/main.js delete mode 100644 src/test/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClientTest.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53dca5f7..9ca098b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: # Run tests - name: Run Tests - run: ./gradlew check -x test + run: ./gradlew check -x test --stacktrace --info # Collect Tests Result of failed tests - name: Collect Tests Result diff --git a/.gitignore b/.gitignore index d2eee4ad..a45cb3b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .idea .qodana +.* build openai.key .run @@ -10,5 +11,5 @@ api.log.json *.log.java *.log api.* -.skynet - +.skynet.bak + diff --git a/CHANGELOG.md b/CHANGELOG.md index e62a29c9..699fa6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## [Unreleased] +## [1.2.18] + +### Improved +- Code Chat +- Various fixes +- Added GPT4 Turbo support + ## [1.2.14] ### Improved diff --git a/build.gradle.kts b/build.gradle.kts index ac9a3e3a..5036926b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,11 +7,11 @@ fun environment(key: String) = providers.environmentVariable(key).get() plugins { id("java") // Java support id("groovy") - id("org.jetbrains.kotlin.jvm") version "1.7.22" - id("org.jetbrains.intellij") version "1.14.1" - id("org.jetbrains.changelog") version "2.0.0" - id("org.jetbrains.qodana") version "0.1.13" - id("org.jetbrains.kotlinx.kover") version "0.6.1" + id("org.jetbrains.kotlin.jvm") version "1.9.20" + id("org.jetbrains.intellij") version "1.16.0" + id("org.jetbrains.changelog") version "2.2.0" + id("org.jetbrains.qodana") version "2023.2.1" + id("org.jetbrains.kotlinx.kover") version "0.7.4" } group = "com.github.simiacryptus" @@ -22,15 +22,14 @@ repositories { maven(url = "https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") } -val kotlin_version = "1.7.22" +val kotlin_version = "1.9.20" val jetty_version = "11.0.15" -val slf4j_version = "2.0.5" -val skyenet_version = "1.0.18" +val slf4j_version = "2.0.9" +val skyenet_version = "1.0.26" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.20") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.27") - implementation(group = "com.simiacryptus.skyenet", name = "util", version = skyenet_version) implementation(group = "com.simiacryptus.skyenet", name = "core", version = skyenet_version) implementation(group = "com.simiacryptus.skyenet", name = "webui", version = skyenet_version) @@ -73,7 +72,6 @@ tasks.named("processResources") { } kotlin { -// jvmToolchain(11) jvmToolchain(17) } @@ -81,7 +79,7 @@ tasks { compileKotlin { kotlinOptions { javaParameters = true - jvmTarget = "1.8" + jvmTarget = "17" } } @@ -167,12 +165,12 @@ changelog { qodana { cachePath.set(file(".qodana").canonicalPath) - reportPath.set(file("build/reports/inspections").canonicalPath) - saveReport.set(true) - showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false) +// reportPath.set(file("build/reports/inspections").canonicalPath) +// saveReport.set(true) +// showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false) } -kover.xmlReport { - onCheck.set(true) -} +//kover.xmlReport { +// onCheck.set(true) +//} diff --git a/gradle.properties b/gradle.properties index 778b814d..716958c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder # SemVer format -> https://semver.org -pluginVersion=1.2.17 +pluginVersion=1.2.18 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html #pluginSinceBuild = 203 pluginSinceBuild=231 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 39834 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 G$^QWPgI3l6 delta 38507 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 { - static Logger logger = LoggerFactory.getLogger(GenerateProjectAction.class) - GenerateProjectAction() { super(false, true) @@ -326,7 +324,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -371,7 +369,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -418,7 +416,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -457,16 +455,17 @@ class GenerateProjectAction extends FileContextAction { return UITools.showDialog(project, SettingsUI.class, Settings.class, "Project Settings", { config -> }) } - SoftwareProjectAI projectAI = new ChatProxy( - clazz: SoftwareProjectAI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() + SoftwareProjectAI projectAI = null @Override File[] processSelection(SelectionState state, Settings config) { + projectAI = new ChatProxy( + clazz: SoftwareProjectAI.class, + api: api, + model: AppSettingsState.instance.defaultChatModel(), + temperature: AppSettingsState.instance.temperature, + deserializerRetries: 2, + ).create() if (config == null) return new File[0] @@ -521,20 +520,20 @@ class GenerateProjectAction extends FileContextAction { entries.each { file, sourceCode -> def relative = trimStart(trimEnd(file.file, '/'), ['/', '.']) ?: "" if (new File(relative).isAbsolute()) { - logger.warn("Invalid path: $relative") + log.warn("Invalid path: $relative") } else { def outFile = new File(outputDir, relative) outFile.parentFile.mkdirs() def best = sourceCode.max { it.code.length() } outFile.text = best.code - logger.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") + log.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") generatedFiles << outFile if (config.saveAlternates) sourceCode.findAll { it != best }.eachWithIndex { alternate, index -> def outFileAlternate = new File(outputDir, relative + ".${index + 1}") outFileAlternate.parentFile.mkdirs() outFileAlternate.text = alternate.code - logger.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") + log.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") generatedFiles << outFileAlternate } } diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy index 018d714e..ded1b1c6 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy @@ -82,7 +82,6 @@ class AnalogueFileAction extends FileContextAction def chatRequest = new ChatRequest() def model = AppSettingsState.instance.defaultChatModel() chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens chatRequest.temperature = AppSettingsState.instance.temperature chatRequest.messages = [ new ChatMessage( diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy index 29a259b7..0f7d37ba 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy @@ -84,7 +84,6 @@ class CreateFileAction extends FileContextAction { def chatRequest = new ChatRequest() def model = AppSettingsState.instance.defaultChatModel() chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens chatRequest.temperature = AppSettingsState.instance.temperature chatRequest.messages = [ //language=TEXT diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy index e97f3be9..6950f56f 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy @@ -186,16 +186,18 @@ class GenerateStoryAction extends FileContextAction( - clazz: AuthorAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() + AuthorAPI proxy = null @Override File[] processSelection(SelectionState state, Settings config) { + proxy = new ChatProxy( + clazz: AuthorAPI.class, + api: api, + model: AppSettingsState.instance.defaultChatModel(), + temperature: AppSettingsState.instance.temperature, + deserializerRetries: 2, + ).create() + List outputFiles = [] if (config) { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ApplicationEvents.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ApplicationEvents.kt deleted file mode 100644 index fc9df4cc..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ApplicationEvents.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.simiacryptus.aicoder - -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools.isSanctioned -import com.intellij.openapi.application.ApplicationActivationListener -import com.intellij.openapi.wm.IdeFrame -import com.simiacryptus.openai.OpenAIClient -import java.io.File - -class ApplicationEvents : ApplicationActivationListener { - val scheduledPool = java.util.concurrent.Executors.newScheduledThreadPool(1) - override fun applicationActivated(ideFrame: IdeFrame) { - if (AppSettingsState.instance.apiLog) { - val file = File(logDir(), "openai.log") - file.deleteOnExit() - OpenAIClient.auxillaryLog = file - } - if(isSanctioned() && false) { - // Open a modal dialog every 5 minutes advising the user to uninstall the plugin - scheduledPool.scheduleAtFixedRate({ - showSanctionedDialog(ideFrame) - }, 5, 5, java.util.concurrent.TimeUnit.MINUTES) - } - super.applicationActivated(ideFrame) - } - - fun showSanctionedDialog(ideFrame: IdeFrame) { - javax.swing.JOptionPane.showConfirmDialog( - /* parentComponent = */ ideFrame.component, - /* message = */ "В связи с войной агрессии, массовыми военными преступлениями и актами геноцида, совершенными Российской Федерацией против Украины и различных других стран, этот плагин не будет работать для вас.", - /* title = */ "AI Coding Assistant - Слава Украине!", - /* optionType = */ javax.swing.JOptionPane.OK_CANCEL_OPTION, - /* messageType = */ javax.swing.JOptionPane.WARNING_MESSAGE - ) - } - - companion object { - fun logDir(): File { - //val baseDir = System.getProperty("java.io.tmpdir") - // baseDir is the canonical idea log directory - var logPath = System.getProperty("idea.log.path") - if (logPath == null) { - logPath = System.getProperty("java.io.tmpdir") - } - if (logPath == null) { - logPath = System.getProperty("user.home") - } - val baseDir = File(logPath) - return baseDir - } - - } -} - diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt index 56aef792..6e55cd88 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt @@ -7,12 +7,11 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.openai.APIClientBase import java.io.File abstract class FileContextAction( - val supportsFiles: Boolean = true, - val supportsFolders: Boolean = true, + private val supportsFiles: Boolean = true, + private val supportsFolders: Boolean = true, ) : BaseAction() { data class SelectionState( @@ -22,15 +21,15 @@ abstract class FileContextAction( abstract fun processSelection(state: SelectionState, config: T?): Array - final override fun handle(event: AnActionEvent) { - val config = getConfig(event.project) - val virtualFile = UITools.getSelectedFile(event) ?: UITools.getSelectedFolder(event) ?: return - val project = event.project ?: return + final override fun handle(e: AnActionEvent) { + val config = getConfig(e.project) + val virtualFile = UITools.getSelectedFile(e) ?: UITools.getSelectedFolder(e) ?: return + val project = e.project ?: return val projectRoot = File(project.basePath!!).toPath() Thread { try { - UITools.redoableTask(event) { - UITools.run(event.project, templateText!!, true) { + UITools.redoableTask(e) { + UITools.run(e.project, templateText!!, true) { val newFiles = try { processSelection( SelectionState( @@ -41,7 +40,7 @@ abstract class FileContextAction( } finally { if (it.isCanceled) throw InterruptedException() } - UITools.writeableFn(event) { + UITools.writeableFn(e) { val files = newFiles.map { file -> val localFileSystem = LocalFileSystem.getInstance() localFileSystem.findFileByIoFile(file.parentFile)?.refresh(false, true) @@ -68,7 +67,7 @@ abstract class FileContextAction( open fun getConfig(project: Project?): T? = null - var isDevAction = false + private var isDevAction = false override fun isEnabled(event: AnActionEvent): Boolean { if (!super.isEnabled(event)) return false if (UITools.isSanctioned()) return false diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt index 17dbee6e..9f5d721f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt @@ -16,12 +16,12 @@ import com.intellij.psi.PsiManager import com.intellij.psi.PsiRecursiveElementVisitor abstract class SelectionAction( - val requiresSelection: Boolean = true + private val requiresSelection: Boolean = true ) : BaseAction() { open fun getConfig(project: Project?): T? = null - fun retarget( + private fun retarget( editorState: EditorState, selectedText: @NlsSafe String?, selectionStart: Int, @@ -42,10 +42,10 @@ abstract class SelectionAction( } } - final override fun handle(event: AnActionEvent) { - val editor = event.getData(CommonDataKeys.EDITOR) ?: return - val config = getConfig(event.project) - val indent = UITools.getIndent(event) + final override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val config = getConfig(e.project) + val indent = UITools.getIndent(e) val caretModel = editor.caretModel val primaryCaret = caretModel.primaryCaret var selectionStart = primaryCaret.selectionStart @@ -57,36 +57,36 @@ abstract class SelectionAction( selectionEnd = end selectionStart = start - UITools.redoableTask(event) { - val document = event.getData(CommonDataKeys.EDITOR)?.document + UITools.redoableTask(e) { + val document = e.getData(CommonDataKeys.EDITOR)?.document var rangeMarker: RangeMarker? = null - WriteCommandAction.runWriteCommandAction(event.project) { + WriteCommandAction.runWriteCommandAction(e.project) { rangeMarker = document?.createGuardedBlock(selectionStart, selectionEnd) } val newText = try { processSelection( - event = event, + event = e, SelectionState( selectedText = selectedText, selectionOffset = selectionStart, selectionLength = selectionEnd - selectionStart, entireDocument = editor.document.text, - language = ComputerLanguage.getComputerLanguage(event), + language = ComputerLanguage.getComputerLanguage(e), indent = indent, contextRanges = editorState.contextRanges, psiFile = editorState.psiFile, - project = event.project + project = e.project ), config = config ) } finally { if(null != rangeMarker) - WriteCommandAction.runWriteCommandAction(event.project) { + WriteCommandAction.runWriteCommandAction(e.project) { document?.removeGuardedBlock(rangeMarker!!) } } - UITools.writeableFn(event) { + UITools.writeableFn(e) { UITools.replaceString(editor.document, selectionStart, selectionEnd, newText) } } @@ -129,7 +129,7 @@ abstract class SelectionAction( ) } - fun contextRanges( + private fun contextRanges( psiFile: PsiFile?, editor: Editor ): Array { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt index 2977daa0..bd1d389f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt @@ -4,7 +4,7 @@ import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project -import com.simiacryptus.skyenet.body.WebSocketServer +import com.simiacryptus.skyenet.sessions.WebSocketServer import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.handler.ContextHandlerCollection import org.eclipse.jetty.webapp.WebAppContext @@ -13,14 +13,14 @@ import org.slf4j.LoggerFactory import java.net.InetSocketAddress class AppServer( - val localName: String, - val port: Int, + private val localName: String, + private val port: Int, project: Project? ) { val log by lazy { LoggerFactory.getLogger(javaClass) } - var domainName: String = "http://$localName:$port" + private var domainName: String = "http://$localName:$port" private val contexts by lazy { val contexts = ContextHandlerCollection() @@ -43,7 +43,7 @@ class AppServer( } } - fun newWebAppContext(server: WebSocketServer, path: String): WebAppContext { + private fun newWebAppContext(server: WebSocketServer, path: String): WebAppContext { val context = WebAppContext() JettyWebSocketServletContainerInitializer.configure(context, null) context.baseResource = server.baseResource @@ -86,7 +86,7 @@ class AppServer( } } - fun isRunning(it: ProgressIndicator) = synchronized(serverLock) { !it.isCanceled && server.isRunning } + private fun isRunning(it: ProgressIndicator) = synchronized(serverLock) { !it.isCanceled && server.isRunning } fun start() { server.start() progressThread.start() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt index 850e907e..a7b5f841 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt @@ -13,15 +13,15 @@ import java.util.* class CodeChatAction : BaseAction() { - override fun handle(event: AnActionEvent) { - val editor = event.getData(CommonDataKeys.EDITOR) ?: return + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return val caretModel = editor.caretModel val primaryCaret = caretModel.primaryCaret val selectedText = primaryCaret.selectedText ?: editor.document.text - val language = ComputerLanguage.getComputerLanguage(event)?.name ?: return - val server = AppServer.getServer(event.project) + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val server = AppServer.getServer(e.project) val uuid = UUID.randomUUID().toString() - server.addApp("/$uuid", CodeChatServer(event.project!!, language, selectedText)) + server.addApp("/$uuid", CodeChatServer(e.project!!, language, selectedText, api = api)) Thread { Thread.sleep(500) try { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt index ef6aaa0b..dc3a5226 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt @@ -1,35 +1,55 @@ package com.github.simiacryptus.aicoder.actions.dev import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.project.Project import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.Heart -import com.simiacryptus.skyenet.body.* -import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter +import com.simiacryptus.skyenet.sessions.* +import com.simiacryptus.skyenet.util.ClasspathResource import org.eclipse.jetty.util.resource.Resource -import java.util.HashMap -import java.util.Map -import java.util.function.Supplier class CodeChatServer( val project: Project, val language: String, val codeSelection: String, -) : SkyenetBasicChat( + val api: OpenAIClient, + resourceBase: String = "codeChat", +) : ChatApplicationBase( applicationName = "Code Chat", - model = AppSettingsState.instance.defaultChatModel() + resourceBase = resourceBase, ) { - val rootOperationID = (0..5).map { ('a'..'z').random() }.joinToString("") - var rootMessageTrail: String = "" + override fun newSession(sessionId: String) = ChatSession( + sessionId = sessionId, + parent = this@CodeChatServer, + model = AppSettingsState.instance.defaultChatModel(), + api = api, + visiblePrompt = """ + |

Code:

+ |
${htmlEscape(codeSelection)}
+ |
+ """.trimMargin().trim(), + hiddenPrompt = "", + systemPrompt = """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following code: + | + |```$language + |$codeSelection + |``` + | + |Responses may use markdown formatting. + """.trimMargin(), + ) - override fun newSession(sessionId: String): CodeChatSession { - val newSession = CodeChatSession(sessionId) -rootMessageTrail = -"""$rootOperationID,

Code:

${htmlEscape(codeSelection)}
""" - newSession.send(rootMessageTrail) - return newSession + override fun processMessage( + sessionId: String, + userMessage: String, + session: PersistentSessionBase, + sessionDiv: SessionDiv, + socket: MessageWebSocket + ) { + TODO("Not yet implemented") } private fun htmlEscape(html: String): String { @@ -38,50 +58,7 @@ rootMessageTrail = .replace("'", "'") } - open inner class CodeChatSession(sessionId: String) : BasicChatSession( - parent = this@CodeChatServer, - model = model, - sessionId = sessionId - ) { - override fun run(userMessage: String) { - var messageTrail = ChatSession.divInitializer() - send("""$messageTrail
$userMessage
$spinner
""") - messages += OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.user, userMessage) - val response = api.chat(chatRequest, model).choices.first()?.message?.content.orEmpty() - messages += OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.assistant, response) - messageTrail += ChatSessionFlexmark.renderMarkdown(response) - send(messageTrail) - } - - override val messages = listOf( - OpenAIClient.ChatMessage( - OpenAIClient.ChatMessage.Role.system, """ - |You are a helpful AI that helps people with coding. - | - |You will be answering questions about the following code: - | - |```$language - |$codeSelection - |``` - | - |Responses may use markdown formatting. - """.trimMargin() - ) - ).toMutableList() - - val chatRequest: OpenAIClient.ChatRequest - get() { - val chatRequest = OpenAIClient.ChatRequest() - val model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = messages.toTypedArray() - return chatRequest - } - } - override val baseResource: Resource - get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) + get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)!!) } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/ConvertFileTo.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/ConvertFileTo.kt index 66c11007..ac41b1d4 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/ConvertFileTo.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/ConvertFileTo.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import java.io.IOException @@ -52,9 +51,6 @@ class ConvertFileTo : ActionGroup() { } companion object { - private val log = Logger.getInstance( - ConvertFileTo::class.java - ) fun getNewFile(project: Project?, file: VirtualFile, language: ComputerLanguage): VirtualFile { val newFileRef = AtomicReference() @@ -83,12 +79,12 @@ class ConvertFileTo : ActionGroup() { class ConvertFileToLanguage(private val targetLanguage: ComputerLanguage) : BaseAction( targetLanguage.name ) { - override fun handle(event: AnActionEvent) { - val sourceLanguage = ComputerLanguage.getComputerLanguage(event) - val indent = UITools.getIndent(event.getData(CommonDataKeys.CARET)) - val virtualFile = event.getData(CommonDataKeys.VIRTUAL_FILE) ?: return - val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return - val project = event.project!! + override fun handle(e: AnActionEvent) { + val sourceLanguage = ComputerLanguage.getComputerLanguage(e) + val indent = UITools.getIndent(e.getData(CommonDataKeys.CARET)) + val virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return + val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + val project = e.project!! Thread { UITools.run(project, "Converting to " + targetLanguage.name) { try { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt deleted file mode 100644 index b0e547cc..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt +++ /dev/null @@ -1,54 +0,0 @@ -@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - -package com.github.simiacryptus.aicoder.actions.dev - -import com.github.simiacryptus.aicoder.actions.BaseAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.util.describe.Description -import java.awt.Desktop -import java.util.UUID - -class LaunchSkyenetAction : BaseAction() { - - override fun isEnabled(event: AnActionEvent): Boolean { - return isEnabled() - } - - interface TestTools { - fun getProject(): Project - fun getSelectedFolder(): VirtualFile - - @Description("Prints to script output") - fun print(msg: String): Unit - } - - override fun handle(e: AnActionEvent) { - val project = e.project!! - val selectedFolder = UITools.getSelectedFolder(e)!! - val server = AppServer.getServer(e.project) - val uuid = UUID.randomUUID().toString() - server.addApp("/$uuid", SkyenetProjectCodingSessionServer(project, selectedFolder)) - Thread { - Thread.sleep(500) - try { - Desktop.getDesktop().browse(server.server.uri.resolve("/$uuid/index.html")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } - - private fun isEnabled(): Boolean { - if (UITools.isSanctioned()) return false - return AppSettingsState.instance.devActions - } - - companion object { - private val log = org.slf4j.LoggerFactory.getLogger(LaunchSkyenetAction::class.java) - } - -} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt index 82ffc921..e9b42bc4 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt @@ -6,7 +6,6 @@ import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.psi.PsiUtil import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.diagnostic.Logger -import com.simiacryptus.openai.APIClientBase /** * The PrintTreeAction class is an IntelliJ action that enables developers to print the tree structure of a PsiFile. @@ -18,8 +17,8 @@ import com.simiacryptus.openai.APIClientBase class PrintTreeAction : BaseAction() { - override fun handle(e1: AnActionEvent) { - log.warn(PsiUtil.printTree(PsiUtil.getLargestContainedEntity(e1)!!)) + override fun handle(e: AnActionEvent) { + log.warn(PsiUtil.printTree(PsiUtil.getLargestContainedEntity(e)!!)) } override fun isEnabled(event: AnActionEvent): Boolean { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt deleted file mode 100644 index 289c01ca..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.dev - -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.skyenet.Heart -import com.simiacryptus.skyenet.OutputInterceptor -import com.simiacryptus.skyenet.body.ClasspathResource -import com.simiacryptus.skyenet.body.SessionServerUtil.asJava -import com.simiacryptus.skyenet.body.SkyenetCodingSessionServer -import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter -import org.eclipse.jetty.util.resource.Resource -import java.util.Map -import java.util.function.Supplier - -class SkyenetProjectCodingSessionServer( - val project: Project, - val selectedFolder: VirtualFile -) : SkyenetCodingSessionServer( - applicationName = "IdeaAgent", - model = AppSettingsState.instance.defaultChatModel(), - apiKey = AppSettingsState.instance.apiKey -) { - override val baseResource: Resource - get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) - - override fun hands() = Map.of( - "ide", - object : LaunchSkyenetAction.TestTools { - override fun getProject(): Project { - return project - } - - override fun getSelectedFolder(): VirtualFile { - return selectedFolder - } - - val toolStream = - OutputInterceptor.createInterceptorStream( - System.out - //PrintStream(NullOutputStream.NULL_OUTPUT_STREAM) - ) - - override fun print(msg: String) { - toolStream.println(msg) - } - } as Object, - ).asJava - - override fun toString(e: Throwable): String { - return e.message ?: e.toString() - } - - override fun heart(hands: Map): Heart = object : WeakGroovyInterpreter(hands) { - override fun wrapExecution(fn: Supplier): T? { - return UITools.run( - project, "Running Script", false - ) { - fn.get() - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt index 3e03dcab..d083d1aa 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.diagnostic.Logger -import com.simiacryptus.openai.APIClientBase import com.simiacryptus.util.audio.AudioRecorder import com.simiacryptus.util.audio.LookbackLoudnessWindowBuffer import java.util.* @@ -22,8 +21,8 @@ import javax.swing.JFrame import javax.swing.JLabel class DictationAction : BaseAction() { - override fun handle(event: AnActionEvent) { - val continueFn = statusDialog(event)::isVisible + override fun handle(e: AnActionEvent) { + val continueFn = statusDialog(e)::isVisible val rawBuffer = ConcurrentLinkedDeque() Thread({ @@ -47,12 +46,12 @@ class DictationAction : BaseAction() { log.warn("Audio processing thread complete") }, "dictation-audio-processor").start() - val caretModel = (event.getData(CommonDataKeys.EDITOR) ?: return).caretModel + val caretModel = (e.getData(CommonDataKeys.EDITOR) ?: return).caretModel val primaryCaret = caretModel.primaryCaret val dictationPump = if (primaryCaret.hasSelection()) { - DictationPump(event, wavBuffer, continueFn, primaryCaret.selectionEnd, primaryCaret.selectedText ?: "") + DictationPump(e, wavBuffer, continueFn, primaryCaret.selectionEnd, primaryCaret.selectedText ?: "") } else { - DictationPump(event, wavBuffer, continueFn, caretModel.offset) + DictationPump(e, wavBuffer, continueFn, caretModel.offset) } Thread({ log.warn("Speech-To-Text thread started") @@ -125,7 +124,7 @@ class DictationAction : BaseAction() { companion object { val log = Logger.getInstance(DictationAction::class.java) - val pool = Executors.newFixedThreadPool(1) + private val pool = Executors.newFixedThreadPool(1) val targetDataLine: Future by lazy { pool.submit { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt index 83a8f953..28109846 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt @@ -5,7 +5,6 @@ import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.UITools.retry import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.simiacryptus.openai.APIClientBase /** * The RedoLast action is an IntelliJ action that allows users to redo the last AI Coder action they performed in the editor. @@ -18,9 +17,9 @@ class RedoLast : BaseAction() { retry[e.getRequiredData(CommonDataKeys.EDITOR).document]!!.run() } - override fun isEnabled(e: AnActionEvent): Boolean { + override fun isEnabled(event: AnActionEvent): Boolean { if (UITools.isSanctioned()) return false - return null != retry[e.getRequiredData(CommonDataKeys.EDITOR).document] + return null != retry[event.getRequiredData(CommonDataKeys.EDITOR).document] } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt index 19995565..47ed45e1 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt @@ -48,9 +48,9 @@ class MarkdownListAction : BaseAction() { return chatProxy.create() } - override fun handle(event: AnActionEvent) { - val caret = event.getData(CommonDataKeys.CARET) ?: return - val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return + override fun handle(e: AnActionEvent) { + val caret = e.getData(CommonDataKeys.CARET) ?: return + val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return val list = getSmallestIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "MarkdownListImpl") ?: return val items = StringUtil.trim( @@ -63,17 +63,17 @@ class MarkdownListAction : BaseAction() { val indent = getIndent(caret) val endOffset = list.textRange.endOffset val bulletTypes = listOf("- [ ] ", "- ", "* ") - val document = (event.getData(CommonDataKeys.EDITOR) ?: return).document + val document = (e.getData(CommonDataKeys.EDITOR) ?: return).document val rawItems = items.map(CharSequence::trim).map { val bulletType = bulletTypes.find(it::startsWith) if (null != bulletType) StringUtil.stripPrefix(it, bulletType).toString() else it.toString() } - UITools.redoableTask(event) { + UITools.redoableTask(e) { var newItems: List? = null UITools.run( - event.project, "Generating New Items", true + e.project, "Generating New Items", true ) { newItems = proxy.newListItems( rawItems, @@ -88,7 +88,7 @@ class MarkdownListAction : BaseAction() { val bulletString = bulletTypes.find(strippedList::startsWith) ?: "1. " newList = newItems?.joinToString("\n") { indent.toString() + bulletString + it } ?: "" } - UITools.writeableFn(event) { + UITools.writeableFn(e) { insertString(document, endOffset, "\n" + newList) } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt index aeb84ae6..61d8b0c9 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -11,7 +11,7 @@ import java.util.stream.Collectors class ActionSettingsRegistry { val actionSettings: MutableMap = HashMap() - val version = 1.91 + private val version = 1.91 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() @@ -52,10 +52,8 @@ class ActionSettingsRegistry { val localCode = actionConfig.file.readText() if (localCode != code) { val element = actionConfig.buildAction(localCode) - if(null != element) { - children.remove(it) - children.add(element) - } + children.remove(it) + children.add(element) } } actionConfig.version = version @@ -69,7 +67,7 @@ class ActionSettingsRegistry { if (!it.file.exists()) return@forEach if (!it.enabled) return@forEach val element = it.buildAction(it.file.readText()) - if(null != element) children.add(element) + children.add(element) } catch (e: Throwable) { UITools.error(log, "Error loading dynamic action", e) } @@ -77,7 +75,7 @@ class ActionSettingsRegistry { return children.toTypedArray() } - class DynamicActionException(cause: Throwable, val msg: String, val file: File, val actionSetting: ActionSettings) : Exception(msg, cause) + class DynamicActionException(cause: Throwable, private val msg: String, val file: File, val actionSetting: ActionSettings) : Exception(msg, cause) data class ActionSettings( val id: String, // Static property @@ -112,7 +110,7 @@ class ActionSettingsRegistry { } } - val packageName: String get() = id.substringBeforeLast('.') + private val packageName: String get() = id.substringBeforeLast('.') val className: String get() = id.substringAfterLast('.') val file: File @@ -146,7 +144,7 @@ class ActionSettingsRegistry { } - fun getActionConfig(action: AnAction): ActionSettings { + private fun getActionConfig(action: AnAction): ActionSettings { return actionSettings.getOrPut(action.javaClass.name) { val actionConfig = ActionSettings(action.javaClass.name) actionConfig.displayText = action.templatePresentation.text @@ -176,10 +174,10 @@ class ActionSettingsRegistry { val log = org.slf4j.LoggerFactory.getLogger(ActionSettingsRegistry::class.java) val actionCache = HashMap() - fun load(actionPackage: String, actionName: String, language: String) = + private fun load(actionPackage: String, actionName: String, language: String) = load("/sources/${language}/$actionPackage/$actionName.$language") - fun load(path: String) = + private fun load(path: String) = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes()?.toString(Charsets.UTF_8) fun load(clazz: Class, language: String) = diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt index edfa5ed1..3e297204 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt @@ -48,7 +48,7 @@ class ActionTable( private val log = Logger.getInstance(ActionTable::class.java) } - val buttonPanel = JPanel() + private val buttonPanel = JPanel() val columnNames = arrayOf("Enabled", "Display Text", "ID") val rowData = actionSettings.map { @@ -85,9 +85,9 @@ class ActionTable( val jtable = JBTable(dataModel) - val scrollpane = JBScrollPane(jtable) + private val scrollpane = JBScrollPane(jtable) - val cloneButton = JButton(object : AbstractAction("Clone") { + private val cloneButton = JButton(object : AbstractAction("Clone") { override fun actionPerformed(e: ActionEvent?) { if (jtable.selectedRows.size != 1) { @@ -154,7 +154,7 @@ class ActionTable( } }) - val editButton = JButton((object : AbstractAction("Edit") { + private val editButton = JButton((object : AbstractAction("Edit") { override fun actionPerformed(e: ActionEvent?) { val id = dataModel.getValueAt(jtable.selectedRow, 2) val actionSetting = actionSettings.find { it.id == id } @@ -174,7 +174,7 @@ class ActionTable( } })) - val removeButton = JButton(object : AbstractAction("Remove") { + private val removeButton = JButton(object : AbstractAction("Remove") { override fun actionPerformed(e: ActionEvent?) { if (jtable.selectedRows.size != 1) { JOptionPane.showMessageDialog(null, "Please select a single row to clone") @@ -189,7 +189,7 @@ class ActionTable( return } rowData.removeIf { - it[2] == selectedSettings?.id + it[2] == selectedSettings.id } this@ActionTable.parent.invalidate() } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt index f9303c71..67815046 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBPasswordField @@ -45,8 +46,8 @@ class AppSettingsComponent { val suppressErrors = JBCheckBox() @Suppress("unused") - @Name("Use GPT-4") - val useGPT4 = JBCheckBox() + @Name("Model") + val modelName = ComboBox() @Suppress("unused") @Name("Enable API Log") @@ -101,6 +102,9 @@ class AppSettingsComponent { init { tokenCounter.isEditable = false + this.modelName.addItem(OpenAIClient.Models.GPT35Turbo.modelName) + this.modelName.addItem(OpenAIClient.Models.GPT4.modelName) + this.modelName.addItem(OpenAIClient.Models.GPT4Turbo.modelName) } val preferredFocusedComponent: JComponent diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt index 461da829..5c0e4e5d 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -7,7 +7,7 @@ import javax.swing.JComponent import javax.swing.JPanel class AppSettingsConfigurable : Configurable { - var settingsComponent: AppSettingsComponent? = null + private var settingsComponent: AppSettingsComponent? = null @Volatile private var mainPanel: JPanel? = null @@ -24,6 +24,7 @@ class AppSettingsConfigurable : Configurable { synchronized(this) { if (null == mainPanel) { settingsComponent = AppSettingsComponent() + reset() mainPanel = UITools.build(settingsComponent!!, false) } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt index c7429e9b..78ee4a5c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -13,7 +13,6 @@ import java.util.* class SimpleEnvelope(var value: String? = null) -@Suppress("MemberVisibilityCanBePrivate") @State(name = "org.intellij.sdk.settings.AppSettingsState", storages = [Storage("SdkSettingsPlugin.xml")]) class AppSettingsState : PersistentStateComponent { val listeningPort: Int = 8081 @@ -24,7 +23,7 @@ class AppSettingsState : PersistentStateComponent { var apiBase = "https://api.openai.com/v1" var apiKey = "" var temperature = 0.1 - var useGPT4 = true + var modelName : String = OpenAIClient.Models.GPT35Turbo.modelName var tokenCounter = 0 var humanLanguage = "English" var devActions = false @@ -33,19 +32,18 @@ class AppSettingsState : PersistentStateComponent { val editorActions = ActionSettingsRegistry() val fileActions = ActionSettingsRegistry() - val recentCommands = mutableMapOf() + private val recentCommands = mutableMapOf() fun createChatRequest(): ChatRequest { return createChatRequest(defaultChatModel()) } - fun defaultChatModel() = if (useGPT4) OpenAIClient.Models.GPT4 else OpenAIClient.Models.GPT35Turbo + fun defaultChatModel() = OpenAIClient.Models.entries.first { it.modelName == modelName } - fun createChatRequest(model: OpenAIClient.Model): ChatRequest { + private fun createChatRequest(model: OpenAIClient.Model): ChatRequest { val chatRequest = ChatRequest() chatRequest.model = model.modelName chatRequest.temperature = temperature - chatRequest.max_tokens = model.maxTokens return chatRequest } @@ -74,7 +72,7 @@ class AppSettingsState : PersistentStateComponent { if (humanLanguage != that.humanLanguage) return false if (apiBase != that.apiBase) return false if (apiKey != that.apiKey) return false - if (useGPT4 != that.useGPT4) return false + if (modelName != that.modelName) return false if (apiLog != that.apiLog) return false if (devActions != that.devActions) return false if (editRequests != that.editRequests) return false @@ -87,7 +85,7 @@ class AppSettingsState : PersistentStateComponent { apiBase, apiKey, temperature, - useGPT4, + modelName, apiLog, devActions, editRequests, diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt index 08860608..78a519a9 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt @@ -8,8 +8,8 @@ import java.util.stream.Collectors class MRUItems { val mostUsedHistory: MutableMap = HashMap() - val mostRecentHistory: MutableList = ArrayList() - var historyLimit = 10 + private val mostRecentHistory: MutableList = ArrayList() + private var historyLimit = 10 fun addInstructionToHistory(instruction: CharSequence) { synchronized(mostRecentHistory) { if(mostRecentHistory.contains(instruction.toString())) { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt index 5f1fc639..848f7adf 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt @@ -1,6 +1,7 @@ package com.github.simiacryptus.aicoder.ui import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.wm.StatusBar @@ -11,14 +12,13 @@ import com.intellij.ui.SimpleListCellRenderer import com.intellij.ui.popup.list.ComboBoxPopup import com.simiacryptus.openai.OpenAIClient import kotlinx.coroutines.CoroutineScope -import java.awt.event.MouseEvent import javax.swing.JList import javax.swing.ListCellRenderer import javax.swing.ListModel class ModelSelectionWidgetFactory : StatusBarWidgetFactory { companion object { - val logger = org.slf4j.LoggerFactory.getLogger(ModelSelectionWidgetFactory::class.java) + val log = org.slf4j.LoggerFactory.getLogger(ModelSelectionWidgetFactory::class.java) } class ModelSelectionWidget : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { @@ -26,6 +26,7 @@ class ModelSelectionWidgetFactory : StatusBarWidgetFactory { private var statusBar: StatusBar? = null private var activeModel: String = AppSettingsState.instance.defaultChatModel().modelName val models = listOf( + OpenAIClient.Models.GPT4Turbo, OpenAIClient.Models.GPT4, OpenAIClient.Models.GPT35Turbo, ) @@ -79,7 +80,7 @@ class ModelSelectionWidgetFactory : StatusBarWidgetFactory { } return ComboBoxPopup(context, activeModel, { str -> activeModel = str - AppSettingsState.instance.useGPT4 = (str == OpenAIClient.Models.GPT4.modelName) + AppSettingsState.instance.modelName = str statusBar?.updateWidget(ID()) }) } @@ -102,10 +103,10 @@ class ModelSelectionWidgetFactory : StatusBarWidgetFactory { } override fun isAvailable(project: Project): Boolean { - return true + return !UITools.isSanctioned() } override fun canBeEnabledOn(statusBar: StatusBar): Boolean { - return true + return !UITools.isSanctioned() } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt index 4b64249c..4c3df59c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt @@ -28,7 +28,7 @@ import javax.swing.event.ChangeListener class TemperatureControlWidgetFactory : StatusBarWidgetFactory { companion object { - val logger = org.slf4j.LoggerFactory.getLogger(TemperatureControlWidgetFactory::class.java) + val log = org.slf4j.LoggerFactory.getLogger(TemperatureControlWidgetFactory::class.java) val pool = Executors.newCachedThreadPool() } @@ -79,21 +79,7 @@ class TemperatureControlWidgetFactory : StatusBarWidgetFactory { val feedbackPanel = JPanel() feedbackPanel.setLayout(VerticalLayout(5)) //feedbackPanel.setBorder(BorderFactory.createTitledBorder("Feedback")) - if (UITools.isSanctioned()) { - feedbackPanel.add( - link( - JBLabel("Why is this plugin disabled?"), - URI("https://news.google.com/home?q=russian+war+crimes&hl=ru&gl=RU&ceid=RU:ru") - ) - ) - feedbackPanel.add( - link( - JBLabel("How can I help?"), - URI("https://www.bing.com/search?q=ukrainian+charities") - ) - ) - tabbedPane.addTab("Info", feedbackPanel) - } else { + if (!UITools.isSanctioned()) { feedbackPanel.add( link( JBLabel("Problem? Request help..."), @@ -148,18 +134,24 @@ class TemperatureControlWidgetFactory : StatusBarWidgetFactory { } override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { + if (UITools.isSanctioned()) return object : StatusBarWidget { + override fun ID(): String = "" + } return TemperatureControlWidget() } override fun createWidget(project: Project): StatusBarWidget { + if (UITools.isSanctioned()) return object : StatusBarWidget { + override fun ID(): String = "" + } return TemperatureControlWidget() } override fun isAvailable(project: Project): Boolean { - return true + return !UITools.isSanctioned() } override fun canBeEnabledOn(statusBar: StatusBar): Boolean { - return true + return !UITools.isSanctioned() } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt index 07a782ee..dc251085 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt @@ -1,5 +1,6 @@ package com.github.simiacryptus.aicoder.ui +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.event.SelectionEvent @@ -21,7 +22,7 @@ import java.util.concurrent.atomic.AtomicBoolean class TokenCountWidgetFactory : StatusBarWidgetFactory { companion object { - val logger = org.slf4j.LoggerFactory.getLogger(TokenCountWidgetFactory::class.java) + val log = org.slf4j.LoggerFactory.getLogger(TokenCountWidgetFactory::class.java) val workQueue = LinkedBlockingDeque() val pool = ThreadPoolExecutor( /* corePoolSize = */ 1, /* maximumPoolSize = */ 1, @@ -124,10 +125,10 @@ class TokenCountWidgetFactory : StatusBarWidgetFactory { } override fun isAvailable(project: Project): Boolean { - return true + return !UITools.isSanctioned() } override fun canBeEnabledOn(statusBar: StatusBar): Boolean { - return true + return !UITools.isSanctioned() } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/BlockComment.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/BlockComment.kt index 1f4d5d2e..56e03e3c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/BlockComment.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/BlockComment.kt @@ -7,14 +7,14 @@ import java.util.* import java.util.stream.Collectors class BlockComment( - val blockPrefix: CharSequence, - val linePrefix: CharSequence, - val blockSuffix: CharSequence, + private val blockPrefix: CharSequence, + private val linePrefix: CharSequence, + private val blockSuffix: CharSequence, indent: CharSequence, vararg textBlock: CharSequence ) : IndentedText(indent, *textBlock) { - class Factory(val blockPrefix: String, val linePrefix: String, val blockSuffix: String) : + class Factory(private val blockPrefix: String, private val linePrefix: String, private val blockSuffix: String) : TextBlockFactory { override fun fromString(text: String?): BlockComment { var text = text!! diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt index c94f88c3..286e8caa 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt @@ -397,7 +397,7 @@ enum class ComputerLanguage(configuration: Configuration) { val docStyle: String val lineComment: TextBlockFactory<*> val blockComment: TextBlockFactory<*> - val docComment: TextBlockFactory<*> + private val docComment: TextBlockFactory<*> init { extensions = listOf(*configuration.fileExtensions) @@ -407,15 +407,6 @@ enum class ComputerLanguage(configuration: Configuration) { docComment = configuration.getDocComments()!! } - fun psiLanguage(): Language? { - return Language.findLanguageByID(name) - } - - val multilineCommentSuffix: CharSequence? - get() = if (docComment is BlockComment.Factory) { - docComment.blockSuffix - } else null - fun getCommentModel(text: String?): TextBlockFactory<*> { if (Objects.requireNonNull(docComment)!!.looksLike(text)) return docComment return if (Objects.requireNonNull(blockComment)!!.looksLike(text)) blockComment else lineComment @@ -436,6 +427,7 @@ enum class ComputerLanguage(configuration: Configuration) { } fun setFileExtensions(vararg fileExtensions: CharSequence): Configuration { + @Suppress("UNCHECKED_CAST") this.fileExtensions = fileExtensions as Array return this } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt index e76cc097..7dbad08a 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt @@ -19,10 +19,10 @@ class IdeaOpenAIClient : OpenAIClient( key = AppSettingsState.instance.apiKey, apiBase = AppSettingsState.instance.apiBase, ) { - val isInRequest = AtomicBoolean(false) + private val isInRequest = AtomicBoolean(false) - override fun incrementTokens(totalTokens: Int) { - AppSettingsState.instance.tokenCounter += totalTokens + override fun incrementTokens(model: Model?, tokens: Int) { + AppSettingsState.instance.tokenCounter += tokens } override fun authorize(request: HttpRequestBase) { @@ -105,12 +105,11 @@ class IdeaOpenAIClient : OpenAIClient( } else { try { if (!AppSettingsState.instance.editRequests) return super.edit(editRequest) - return withJsonDialog(editRequest, { - val editRequest = it + return withJsonDialog(editRequest, { request -> UITools.run( lastEvent!!.project, "OpenAI Request", true, suppressProgress = false ) { - super.edit(editRequest) + super.edit(request) } }, "Edit Edit Request") } finally { @@ -123,7 +122,7 @@ class IdeaOpenAIClient : OpenAIClient( val api: OpenAIClient get() = IdeaOpenAIClient() var lastEvent: AnActionEvent? = null - fun uiEdit( + private fun uiEdit( project: Project? = null, title: String = "Edit Request", jsonTxt: String @@ -164,7 +163,7 @@ class IdeaOpenAIClient : OpenAIClient( } ?: jsonTxt } - fun execute( + private fun execute( fn: () -> T ): T? { val application = ApplicationManager.getApplication() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt index e22c5a8d..fdbaa598 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt @@ -8,16 +8,16 @@ import java.util.stream.Collectors class LineComment(private val commentPrefix: CharSequence, indent: CharSequence?, vararg lines: CharSequence) : IndentedText(indent!!, *lines) { - class Factory(val commentPrefix: String) : TextBlockFactory { + class Factory(private val commentPrefix: String) : TextBlockFactory { override fun fromString(text: String?): LineComment { - var text = text - text = text!!.replace(Regex("\t"), TextBlock.TAB_REPLACEMENT.toString()) - val indent = getWhitespacePrefix(*text.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + var textVar = text + textVar = textVar!!.replace(Regex("\t"), TextBlock.TAB_REPLACEMENT.toString()) + val indent = getWhitespacePrefix(*textVar.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } .toTypedArray()) return LineComment( commentPrefix, indent, - *Arrays.stream(text.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + *Arrays.stream(textVar.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } .toTypedArray()) .map { s: String? -> stripPrefix( @@ -52,4 +52,4 @@ class LineComment(private val commentPrefix: CharSequence, indent: CharSequence? override fun withIndent(indent: CharSequence?): LineComment { return LineComment(commentPrefix, indent, *lines) } -} +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt index 0ccc5e48..50a917ea 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -59,6 +59,7 @@ import javax.swing.* import javax.swing.text.JTextComponent import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 +import kotlin.reflect.KVisibility import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaField @@ -247,64 +248,6 @@ object UITools { } } - @JvmStatic - fun readJavaUI(component: Any, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - Arrays.stream(componentClass.fields).map { obj: Field -> obj.name }.collect(Collectors.toSet()) - for (settingsField in settings.javaClass.fields) { - if (Modifier.isStatic(settingsField.modifiers)) continue - settingsField.isAccessible = true - val settingsFieldName = settingsField.name - try { - var newSettingsValue: Any? = null - if (!declaredUIFields.contains(settingsFieldName)) continue - val uiField = componentClass.getDeclaredField(settingsFieldName) - var uiVal = uiField[component] - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.type.name) { - "java.lang.String" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text - } else if (uiVal is ComboBox<*>) { - newSettingsValue = uiVal.item - } - - "int" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text.toInt() - } - - "long" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text.toLong() - } - - "double" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text.toDouble() - } - - "boolean" -> if (uiVal is JCheckBox) { - newSettingsValue = uiVal.isSelected - } else if (uiVal is JTextComponent) { - newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) - } - - else -> if (Enum::class.java.isAssignableFrom(settingsField.type)) { - if (uiVal is ComboBox<*>) { - val comboBox = uiVal - val item = comboBox.item - newSettingsValue = - findValue(settingsField.type as Class?>, item.toString()) - } - } - } - settingsField[settings] = newSettingsValue - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - @JvmStatic fun readKotlinUI(component: R, settings: T) { val componentClass: Class<*> = component.javaClass @@ -377,62 +320,16 @@ object UITools { ) } - @JvmStatic - fun writeJavaUI(component: Any, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - Arrays.stream(componentClass.fields).map { obj: Field -> obj.name }.collect(Collectors.toSet()) - for (settingsField in settings.javaClass.fields) { - val fieldName = settingsField.name - try { - if (!declaredUIFields.contains(fieldName)) continue - val uiField = componentClass.getDeclaredField(fieldName) - val settingsVal = settingsField.get(settings) ?: continue - var uiVal = uiField[component] - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.type.name) { - "java.lang.String" -> if (uiVal is JTextComponent) { - uiVal.text = settingsVal.toString() - } else if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - - "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toString() - } - - "long" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toLong().toString() - } - - "boolean" -> if (uiVal is JCheckBox) { - uiVal.isSelected = (settingsVal as Boolean) - } else if (uiVal is JTextComponent) { - uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) - } - - "double", "java.lang.Double" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Double).toString() - } - - else -> if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - } - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - @JvmStatic fun writeKotlinUI(component: R, settings: T) { val componentClass: Class<*> = component.javaClass val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() - for (settingsField in settings.javaClass.kotlin.memberProperties) { + val memberProperties = settings.javaClass.kotlin.memberProperties + val publicProperties = memberProperties.filter { + it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> + } + for (settingsField in publicProperties) { val fieldName = settingsField.name try { if (!declaredUIFields.contains(fieldName)) continue @@ -478,32 +375,6 @@ object UITools { } } - @JvmStatic - fun addJavaFields(ui: Any, formBuilder: FormBuilder) { - var first = true - for (field in ui.javaClass.fields) { - if (Modifier.isStatic(field.modifiers)) continue - try { - val nameAnnotation = field.getDeclaredAnnotation(Name::class.java) - val component = field[ui] as JComponent - if (nameAnnotation != null) { - if (first) { - first = false - formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) - } else { - formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) - } - } else { - formBuilder.addComponentToRightColumn(component, 1) - } - } catch (e: IllegalAccessException) { - throw RuntimeException(e) - } catch (e: Throwable) { - error(log, "Error processing " + field.name, e) - } - } - } - @JvmStatic fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { var first = true @@ -642,18 +513,6 @@ object UITools { return JOptionPane.CLOSED_OPTION } - @JvmStatic - fun configureTextArea(textArea: JBTextArea): JBTextArea { - val font = textArea.font - val fontMetrics = textArea.getFontMetrics(font) - textArea.preferredSize = Dimension( - (fontMetrics.charWidth(' ') * textArea.columns * 1.2).toInt(), - (fontMetrics.height * textArea.rows * 1.2).toInt() - ) - textArea.autoscrolls = true - return textArea - } - @JvmStatic fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { val scrollPane = JBScrollPane(promptArea) @@ -965,7 +824,7 @@ object UITools { actionLog += message } - val singleThreadPool = Executors.newSingleThreadExecutor() + private val singleThreadPool = Executors.newSingleThreadExecutor() @JvmStatic fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { @@ -1240,35 +1099,5 @@ object UITools { return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value } -@JvmStatic fun showInputDialog( - parentComponent: Component?, - message: Any?, - title: String?, - messageType: Int, - mru: List, - providedOptions: List? -): Any? { - val icon = null - val selectionValues = (mru + (providedOptions ?: emptyList())).toTypedArray() - val initialSelectionValue = null - val pane = JOptionPane( - message, - messageType, - JOptionPane.OK_CANCEL_OPTION, - icon, - null, - null - ) - pane.wantsInput = true - pane.selectionValues = selectionValues - pane.initialSelectionValue = initialSelectionValue - val dialog = pane.createDialog(parentComponent, title) - pane.selectInitialValue() - dialog.show() - dialog.dispose() - val value = pane.inputValue - return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value -} - } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt index 1a35cb07..04c9a25e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt @@ -5,7 +5,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile -class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boolean, val language: ComputerLanguage) { +class PsiClassContext(val text: String, private val isPrior: Boolean, private val isOverlap: Boolean, val language: ComputerLanguage) { val children = ArrayList() /** @@ -142,8 +142,5 @@ class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boo return PsiClassContext("", false, true, language).init(psiFile, selectionStart, selectionEnd) } - fun getContext(language: ComputerLanguage, psiFile: PsiFile): PsiClassContext { - return getContext(psiFile, 0, psiFile.textLength, language) - } } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt index b2f90a1a..92353cf1 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt @@ -18,7 +18,7 @@ import java.util.regex.Pattern class PsiTranslationTree( private val stubId: String?, - val prefix: String?, + private val prefix: String?, private val elementText: String?, val sourceLanguage: ComputerLanguage, val targetLanguage: ComputerLanguage, @@ -26,7 +26,7 @@ class PsiTranslationTree( val suffix = StringBuffer() val children = ArrayList() - fun getStubRegex( + private fun getStubRegex( targetLanguage: ComputerLanguage?, translatedOuter: CharSequence?, ): Regex { @@ -97,7 +97,7 @@ class PsiTranslationTree( fun getTranslatedDocument(): CharSequence = try { var translated = translatedResult.toString() if (!stubs.isEmpty()) { - logger.warn( + log.warn( "Translating ${stubs.size} stubs in ${stubId ?: "---"} - Initial Code: \n```\n ${ translationText().replace( "\n", @@ -112,7 +112,7 @@ class PsiTranslationTree( val regex = child.getStubRegex(targetLanguage, translated) val childDoc = child.getTranslatedDocument().toString() val findAll = regex.findAll(translated).toList() - logger.warn( + log.warn( "Replacing ${findAll.size} instances of ${child.stubId} with: \n```\n ${ childDoc.replace( "\n", @@ -123,7 +123,7 @@ class PsiTranslationTree( translated.replace(regex, childDoc.replace("\$", "\\\$")) } } - logger.warn("Translation for ${stubId ?: "---"}: \n```\n ${translated.replace("\n", "\n ")}\n```\n") + log.warn("Translation for ${stubId ?: "---"}: \n```\n ${translated.replace("\n", "\n ")}\n```\n") translated } catch (e: InterruptedException) { throw RuntimeException(e) @@ -223,7 +223,7 @@ class PsiTranslationTree( companion object { - val logger = Logger.getInstance(PsiTranslationTree::class.java) + val log = Logger.getInstance(PsiTranslationTree::class.java) fun parseFile( psiFile: PsiFile, diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt index 154c474d..ba5a4495 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt @@ -2,11 +2,9 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.editor.Caret import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiFile import com.simiacryptus.util.StringUtil import java.util.* import java.util.concurrent.atomic.AtomicReference @@ -28,11 +26,6 @@ object PsiUtil { "Comment" ) - @JvmStatic - fun getLargestIntersectingComment(element: PsiElement, selectionStart: Int, selectionEnd: Int): PsiElement? { - return getLargestIntersecting(element, selectionStart, selectionEnd, *ELEMENTS_COMMENTS) - } - fun getAll(element: PsiElement, vararg types: CharSequence): List { val elements: MutableList = ArrayList() val visitor = AtomicReference() @@ -50,38 +43,6 @@ object PsiUtil { return elements } - @JvmStatic - private fun getLargestIntersecting( - element: PsiElement, - selectionStart: Int, - selectionEnd: Int, - vararg types: CharSequence - ): PsiElement? { - val largest = AtomicReference(null) - val visitor = AtomicReference() - visitor.set(object : PsiElementVisitor() { - override fun visitElement(element: PsiElement) { - val textRange = element.textRange - val within = - within( - textRange, - selectionStart - ) && textRange.startOffset <= selectionEnd && textRange.endOffset + 1 >= selectionEnd - if (matchesType(element, *types)) { - if (within) { - largest.updateAndGet { s: PsiElement? -> - if ((s?.text?.length ?: 0) > element.text.length) s else element - } - } - } - super.visitElement(element) - element.acceptChildren(visitor.get()) - } - }) - element.accept(visitor.get()) - return largest.get() - } - @JvmStatic fun getSmallestIntersecting( element: PsiElement, @@ -189,13 +150,13 @@ object PsiUtil { } private fun getName(elementClass: Class<*>): String { - var elementClass = elementClass + var elementClassVar = elementClass val stringBuilder = StringBuilder() - val interfaces = getInterfaces(elementClass) - while (elementClass != Any::class.java) { + val interfaces = getInterfaces(elementClassVar) + while (elementClassVar != Any::class.java) { if (stringBuilder.isNotEmpty()) stringBuilder.append("/") - stringBuilder.append(elementClass.simpleName) - elementClass = elementClass.superclass + stringBuilder.append(elementClassVar.simpleName) + elementClassVar = elementClassVar.superclass } stringBuilder.append("[ ") stringBuilder.append(interfaces.stream().sorted().collect(Collectors.joining(","))) @@ -243,10 +204,6 @@ object PsiUtil { return psiFile } - @JvmStatic - fun getSmallestIntersectingMajorCodeElement(psiFile: PsiFile, caret: Caret) = - getCodeElement(psiFile, caret.selectionStart, caret.selectionEnd) - @JvmStatic fun getCodeElement( psiFile: PsiElement?, diff --git a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt index 6d29f4a0..a0a89ab7 100644 --- a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt +++ b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt @@ -5,6 +5,7 @@ package com.simiacryptus.skyenet.heart import com.simiacryptus.skyenet.Heart import java.lang.reflect.Method +@Suppress("unused") open class WeakGroovyInterpreter(defs: java.util.Map) : Heart { private val shell: Any diff --git a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt index e809a717..5827ef99 100644 --- a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt +++ b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt @@ -3,6 +3,7 @@ import com.simiacryptus.skyenet.Heart import java.lang.reflect.Method +@Suppress("unused") open class WeakKotlinInterpreter( defs: Map = mapOf(), ) : Heart { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7920fb73..7e9f888c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -13,10 +13,6 @@ displayName="AI Coder"/> - - - + - - - - - diff --git a/src/main/resources/codeChat/chat.css b/src/main/resources/codeChat/chat.css new file mode 100644 index 00000000..b1e6adaa --- /dev/null +++ b/src/main/resources/codeChat/chat.css @@ -0,0 +1,278 @@ +#messages { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + overflow-y: auto; + padding: 10px; + padding-top: 40px; + flex-grow: 1; + flex-shrink: 1; +} + +.message { + background-color: #f0f0f0; + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; + overflow: scroll; + z-index: 100; +} + +#form { + position: absolute; + left: 0; + right: 0; + bottom: 0; + display: flex; + margin: 0; + padding: 5px; + flex-shrink: 0; +} + +.reply-form { + width: 100%; +} + +.chat-input { + resize: vertical; +} + +.reply-input { + resize: vertical; +} + +.href-link { + text-decoration: underline; + color: blue; +} + +#message { + flex-grow: 1; + margin-right: 5px; +} + +#disconnected-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + display: flex; + justify-content: center; + align-items: center; + color: white; + font-size: 24px; +} + +#disconnected-overlay p { + font-size: 3rem; + line-height: 1.5; + margin-bottom: 20px; + animation-name: bounce; + animation-duration: 0.5s; + animation-iteration-count: infinite; + animation-direction: alternate; + left: 10%; + position: relative; + color: firebrick; +} + +@keyframes bounce { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-10px); + } +} + +.spinner-border { + display: block; + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + + +#toolbar { + background-color: #f1f1f1; + padding: 5px; + position: fixed; + top: 0; + width: 100%; + horiz-align: left; + z-index: 1; +} + +#toolbar a { + color: #000; + text-decoration: none; + padding: 5px; +} + +#toolbar a:hover { + background-color: #ddd; +} + + +#namebar { + background-color: #f1f1f1; + padding: 5px; + position: fixed; + top: 0; + right: 0; + horiz-align: right; + z-index: 2; +} + +#namebar a { + color: #000; + text-decoration: none; + padding: 5px; +} + +#namebar a:hover { + background-color: #ddd; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; + position: relative; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +pre { + white-space: pre-wrap; /* Since CSS modules do not automatically apply this */ +} + +#container { + display: flex; + flex-direction: column; + height: 100vh; /* Adjust this value based on your preferred container height */ +} + +.play-button { + font-size: 48px; + font-weight: bold; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.play-button:focus { + outline: none; +} + +.play-button:active { + transform: scale(0.9); +} + + +.regen-button { + font-size: 48px; + font-weight: bold; + color: chartreuse; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.regen-button:focus { + outline: none; +} + +.regen-button:active { + transform: scale(0.9); +} + +.message-container { + position: relative; /* Add this line to set the position property of the parent div */ +} + +.cancel-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 16px; + font-weight: bold; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.cancel-button:focus { + outline: none; +} + +.cancel-button:active { + transform: scale(0.9); +} + +.collapsible-content { + overflow: hidden; + max-height: 0; + transition: max-height 0.2s ease-out; +} diff --git a/src/main/resources/codeChat/chat.js b/src/main/resources/codeChat/chat.js new file mode 100644 index 00000000..f946c020 --- /dev/null +++ b/src/main/resources/codeChat/chat.js @@ -0,0 +1,65 @@ + +let socket; + +function getSessionId() { + if (!window.location.hash) { + fetch('newSession') + .then(response => { + if (response.ok) { + return response.text(); + } else { + throw new Error('Failed to get new session ID'); + } + }) + .then(sessionId => { + window.location.hash = sessionId; + connect(sessionId); + }); + } else { + return window.location.hash.substring(1); + } +} + +function send(message) { + console.log('Sending message:', message); + socket.send(message); +} + +function connect(sessionId, customReceiveFunction) { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.hostname; + const port = window.location.port; + let path = window.location.pathname; + let strings = path.split('/'); + if(strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html') { + path = '/' + strings[1] + '/'; + } else { + path = '/'; + } + + socket = new WebSocket(`${protocol}//${host}:${port}${path}ws?sessionId=${sessionId}`); + + socket.addEventListener('open', (event) => { + console.log('WebSocket connected:', event); + showDisconnectedOverlay(false); + }); + + socket.addEventListener('message', customReceiveFunction || onWebSocketText); + + socket.addEventListener('close', (event) => { + console.log('WebSocket closed:', event); + showDisconnectedOverlay(true); + setTimeout(() => { + connect(getSessionId(), customReceiveFunction); + }, 3000); + }); + + socket.addEventListener('error', (event) => { + console.error('WebSocket error:', event); + }); +} + +function showDisconnectedOverlay(show) { + const overlay = document.getElementById('disconnected-overlay'); + overlay.style.display = show ? 'block' : 'none'; +} diff --git a/src/main/resources/codeChat/favicon.svg b/src/main/resources/codeChat/favicon.svg new file mode 100644 index 00000000..2cbd9fdb --- /dev/null +++ b/src/main/resources/codeChat/favicon.svg @@ -0,0 +1,724 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/codeChat/index.html b/src/main/resources/codeChat/index.html new file mode 100644 index 00000000..79979eb8 --- /dev/null +++ b/src/main/resources/codeChat/index.html @@ -0,0 +1,37 @@ + + + + + WebSocket Client + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+

Disconnected. Attempting to reconnect...

+
+ + + + diff --git a/src/main/resources/codeChat/main.js b/src/main/resources/codeChat/main.js new file mode 100644 index 00000000..2d899aa1 --- /dev/null +++ b/src/main/resources/codeChat/main.js @@ -0,0 +1,128 @@ +function showModal(endpoint) { + fetchData(endpoint); + document.getElementById('modal').style.display = 'block'; +} + +function closeModal() { + document.getElementById('modal').style.display = 'none'; +} + +async function fetchData(endpoint) { + try { + // Add session id to the endpoint as a path parameter + const sessionId = getSessionId(); + if (sessionId) { + endpoint = endpoint + "?sessionId=" + sessionId; + } + const response = await fetch(endpoint); + const text = await response.text(); + document.getElementById('modal-content').innerHTML = "
" + text + "
"; + Prism.highlightAll(); + } catch (error) { + console.error('Error fetching data:', error); + } +} + +let messageVersions = {}; + +function onWebSocketText(event) { + console.log('WebSocket message:', event); + const messagesDiv = document.getElementById('messages'); + + // Parse message e.g. "id,version,content" + const firstCommaIndex = event.data.indexOf(','); + const secondCommaIndex = event.data.indexOf(',', firstCommaIndex + 1); + const messageId = event.data.substring(0, firstCommaIndex); + const messageVersion = event.data.substring(firstCommaIndex + 1, secondCommaIndex); + const messageContent = event.data.substring(secondCommaIndex + 1); + // If messageVersion isn't more than the version for the messageId using the version map, then ignore the message + if (messageVersion <= (messageVersions[messageId] || 0)) { + console.log("Ignoring message with id " + messageId + " and version " + messageVersion); + return; + } else { + messageVersions[messageId] = messageVersion; + } + + let messageDiv = document.getElementById(messageId); + + if (messageDiv) { + messageDiv.innerHTML = messageContent; + } else { + messageDiv = document.createElement('div'); + messageDiv.className = 'message message-container'; // Add the message-container class + messageDiv.id = messageId; + messageDiv.innerHTML = messageContent; + messagesDiv.appendChild(messageDiv); + } + + messagesDiv.scrollTop = messagesDiv.scrollHeight; + Prism.highlightAll(); +} + +document.addEventListener('DOMContentLoaded', () => { + + document.querySelector('.close').addEventListener('click', closeModal); + + window.addEventListener('click', (event) => { + if (event.target === document.getElementById('modal')) { + closeModal(); + } + }); + + const form = document.getElementById('form'); + const messageInput = document.getElementById('message'); + + form.addEventListener('submit', (event) => { + event.preventDefault(); + send(messageInput.value); + messageInput.value = ''; + }); + + messageInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + form.dispatchEvent(new Event('submit')); + } + }); + + connect(undefined, onWebSocketText); + + document.body.addEventListener('click', (event) => { + const target = event.target; + if (target.classList.contains('play-button')) { + const messageId = target.getAttribute('data-id'); + send('!' + messageId + ',run'); + } else if (target.classList.contains('regen-button')) { + const messageId = target.getAttribute('data-id'); + send('!' + messageId + ',regen'); + } else if (target.classList.contains('cancel-button')) { + const messageId = target.getAttribute('data-id'); + send('!' + messageId + ',stop'); + } else if (target.classList.contains('href-link')) { + const messageId = target.getAttribute('data-id'); + send('!' + messageId + ',link'); + } else if (target.classList.contains('text-submit-button')) { + const messageId = target.getAttribute('data-id'); + const text = document.querySelector('.reply-input[data-id="' + messageId + '"]').value; + send('!' + messageId + ',userTxt,' + text); + } + }); + + fetch('appInfo') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.applicationName) { + document.title = data.applicationName; + } + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); + +}); + diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy index 7c381b64..49c7835f 100644 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy +++ b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy @@ -22,8 +22,6 @@ import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream class GenerateProjectAction extends FileContextAction { - static Logger logger = LoggerFactory.getLogger(GenerateProjectAction.class) - GenerateProjectAction() { super(false, true) @@ -326,7 +324,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -371,7 +369,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -418,7 +416,7 @@ class GenerateProjectAction extends FileContextAction { ) // def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts // progress(progressVal) -// logger.info("Progress: $progressVal") +// log.info("Progress: $progressVal") return new Pair(file.location, implement) } }) @@ -457,16 +455,17 @@ class GenerateProjectAction extends FileContextAction { return UITools.showDialog(project, SettingsUI.class, Settings.class, "Project Settings", { config -> }) } - SoftwareProjectAI projectAI = new ChatProxy( - clazz: SoftwareProjectAI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() + SoftwareProjectAI projectAI = null @Override File[] processSelection(SelectionState state, Settings config) { + projectAI = new ChatProxy( + clazz: SoftwareProjectAI.class, + api: api, + model: AppSettingsState.instance.defaultChatModel(), + temperature: AppSettingsState.instance.temperature, + deserializerRetries: 2, + ).create() if (config == null) return new File[0] @@ -521,20 +520,20 @@ class GenerateProjectAction extends FileContextAction { entries.each { file, sourceCode -> def relative = trimStart(trimEnd(file.file, '/'), ['/', '.']) ?: "" if (new File(relative).isAbsolute()) { - logger.warn("Invalid path: $relative") + log.warn("Invalid path: $relative") } else { def outFile = new File(outputDir, relative) outFile.parentFile.mkdirs() def best = sourceCode.max { it.code.length() } outFile.text = best.code - logger.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") + log.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") generatedFiles << outFile if (config.saveAlternates) sourceCode.findAll { it != best }.eachWithIndex { alternate, index -> def outFileAlternate = new File(outputDir, relative + ".${index + 1}") outFileAlternate.parentFile.mkdirs() outFileAlternate.text = alternate.code - logger.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") + log.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") generatedFiles << outFileAlternate } } diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy index 018d714e..ded1b1c6 100644 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy +++ b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy @@ -82,7 +82,6 @@ class AnalogueFileAction extends FileContextAction def chatRequest = new ChatRequest() def model = AppSettingsState.instance.defaultChatModel() chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens chatRequest.temperature = AppSettingsState.instance.temperature chatRequest.messages = [ new ChatMessage( diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy index 29a259b7..0f7d37ba 100644 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy +++ b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy @@ -84,7 +84,6 @@ class CreateFileAction extends FileContextAction { def chatRequest = new ChatRequest() def model = AppSettingsState.instance.defaultChatModel() chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens chatRequest.temperature = AppSettingsState.instance.temperature chatRequest.messages = [ //language=TEXT diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy index e97f3be9..6950f56f 100644 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy +++ b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy @@ -186,16 +186,18 @@ class GenerateStoryAction extends FileContextAction( - clazz: AuthorAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() + AuthorAPI proxy = null @Override File[] processSelection(SelectionState state, Settings config) { + proxy = new ChatProxy( + clazz: AuthorAPI.class, + api: api, + model: AppSettingsState.instance.defaultChatModel(), + temperature: AppSettingsState.instance.temperature, + deserializerRetries: 2, + ).create() + List outputFiles = [] if (config) { diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt index 40cd1c61..17d7597b 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt @@ -98,126 +98,6 @@ class ApplicationDevelopmentUITest { ) } - private fun test_problem_red_black_tree(): Pair> { - return Pair( - "Red_Black_Tree", - listOf( - "Implement a utility function to insert a new node into the tree.", - "Implement a utility function to remove a node from the tree.", - "Implement a utility function to find a node in the tree.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - - private fun test_problem_morse(): Pair> { - return Pair( - "Text_to_Morse", - listOf( - "Implement a utility function that converts text to Morse code.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - - private fun test_problem_permutations(): Pair> { - return Pair( - "Permutations", - listOf( - "Implement class members needed to represent a single permutation.", - // Group-theory operations - "Implement a member function that returns the inverse permutation.", - "Implement a member function that returns the composition of two permutations.", - // Testing - "Implement a utility function that generates all permutations of a string.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_prime(): Pair> { - return Pair( - "Prime", - listOf( - "Implement a utility function that returns true if the given number is prime.", - "Implement a utility function that returns the next prime number after the given number.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_fibonacci(): Pair> { - return Pair( - "Fibonacci", - listOf( - "Implement a utility function that returns the nth number in the Fibonacci sequence.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_sort(): Pair> { - return Pair( - "Sort", - listOf( - "Implement a utility function that returns the given array sorted.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_search(): Pair> { - return Pair( - "Search", - listOf( - "Implement a utility function that returns the index of the given element in the given array.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_reverse(): Pair> { - return Pair( - "Reverse", - listOf( - "Implement a utility function that returns the given array in reverse order.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_merge(): Pair> { - return Pair( - "Merge", - listOf( - "Implement a utility function that merges two sorted arrays into a single sorted array.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_split(): Pair> { - return Pair( - "Split", - listOf( - "Implement a utility function that splits a sorted array into two sorted arrays.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - - private fun test_problem_factorial(): Pair> { - return Pair( - "Factorial", - listOf( - "Implement a utility function that returns the factorial of the given number.", - "Implement a static main method that tests each class member and validates the output." - ) - ) - } - private fun test( name: String, @@ -229,8 +109,7 @@ class ApplicationDevelopmentUITest { ) { val reportPrefix = "${name}_${language}_" val testOutputFile = File(outputDir, "${name}_${language}.md") - val out = PrintWriter(FileOutputStream(testOutputFile)) - out.use { out -> + PrintWriter(FileOutputStream(testOutputFile)).use { out -> out.println( """ diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/DocGen.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/DocGen.kt index da7dec56..592fd5d9 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/DocGen.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/DocGen.kt @@ -86,19 +86,6 @@ class DocGen { writer.close() } - private fun printTable( - writer: PrintWriter, - actions: Array> - ) { - writer.println("| Text | Description | Shortcut | Full Description |") - writer.println("| --- | --- | --- | --- |") - actions.forEach { action -> - val file = "src/main/kotlin/${action["id"].toString().replace(".", "/")}.kt" - val uiText = action["text"].toString().replace("_", "") - writer.println("| [$uiText]($file) | ${action["description"]} | ${action["shortcut"]} | ${action["long_description"]} |") - } - } - private fun printTable2( writer: PrintWriter, actions: Array> diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt index 06ebaf2b..ae5e8890 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt @@ -5,7 +5,6 @@ package com.github.simiacryptus.aicoder import com.github.simiacryptus.aicoder.UITestUtil.Companion.canRunTests import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentJavaImplementation import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentMarkdownListAppend -import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentMarkdownTableOps import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentTextAppend import com.github.simiacryptus.aicoder.UITestUtil.Companion.outputDir import org.junit.Test diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt index 700867ad..faf79460 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt @@ -60,9 +60,6 @@ class UITestUtil { byXpath("//div[contains(@accessiblename.key, 'editor.accessible.name')]") ).isNotEmpty() - private fun isDialogOpen(): Boolean = - robot.findAll(ComponentFixture::class.java, byXpath("//div[@class='EngravedLabel']")).isNotEmpty() - private fun isStillRunning(): Boolean { val resultText = componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]") @@ -70,7 +67,7 @@ class UITestUtil { } - fun componentText(s: String): String { + private fun componentText(s: String): String { return getLines(getComponent(s)) } @@ -81,7 +78,7 @@ class UITestUtil { .reduceOrNull { a, b -> a + "\n" + b }.orEmpty() } - fun findText(element: ComponentFixture, text: String): Pair? { + private fun findText(element: ComponentFixture, text: String): Pair? { val lines: Map> = element.data.getAll().groupBy { it.point.y } val line = lines.filter { it.value.map { it.text }.reduce { a, b -> a + b }.contains(text) }.firstOrNull()?.value @@ -126,17 +123,12 @@ class UITestUtil { element.runJs("robot.releaseMouseButtons()") } - fun clickText(element: ComponentFixture, text: String) { + private fun clickText(element: ComponentFixture, text: String) { val (leftPoint, _) = findText(element, text) ?: throw Exception("Could not find text $text") element.runJs("robot.click(component, new Point(${leftPoint.x}, ${leftPoint.y}))") } - fun rightClickText(element: ComponentFixture, text: String) { - val (leftPoint, _) = findText(element, text) ?: throw Exception("Could not find text $text") - element.runJs("robot.rightClick(component, new Point(${leftPoint.x}, ${leftPoint.y}))") - } - - fun implementCode(prompt: String): BufferedImage { + private fun implementCode(prompt: String): BufferedImage { click("//div[@class='EditorComponentImpl']") keyboard.selectAll() keyboard.key(KeyEvent.VK_DELETE) @@ -170,7 +162,7 @@ class UITestUtil { * This function will wait until the process is started, then wait until it is finished. * It will also print out the total time the process took to complete. */ - fun awaitRunCompletion() { + private fun awaitRunCompletion() { val timeout = 1 * 60 * 1000 val startOverall = System.currentTimeMillis() @@ -195,28 +187,6 @@ class UITestUtil { println("Process ended after ${(end - start) / 1000.0}") } - fun awaitDialog() { - // Await a dialog - it must be open then closed - val timeout = 1 * 60 * 1000 - val startOverall = System.currentTimeMillis() - while (!isDialogOpen()) { - sleep(100) - if (System.currentTimeMillis() - startOverall > timeout) { - throw RuntimeException("Timeout waiting for dialog to open") - } - } - val start = System.currentTimeMillis() - println("Dialog opened") - while (isDialogOpen()) { - sleep(100) - if (System.currentTimeMillis() - start > timeout) { - throw RuntimeException("Timeout waiting for dialog to close") - } - } - val end = System.currentTimeMillis() - println("Dialog closed after ${(end - start) / 1000.0}") - } - fun awaitBackgroundProgress() { // Await a background task - it must be open then closed val timeout = 1 * 60 * 1000 @@ -247,8 +217,7 @@ class UITestUtil { if (progressPanel.isEmpty()) return false val componentFixture = progressPanel.get(0) val labels = componentFixture.data.getAll() - if (labels.size == 0) return false - return true + return labels.size != 0 } fun documentJavaImplementation( @@ -269,7 +238,7 @@ class UITestUtil { ) } - fun documentImplementation( + private fun documentImplementation( name: String, out1: PrintWriter, directive: String, @@ -653,106 +622,6 @@ class UITestUtil { fun getEditor() = getComponent("//div[@class='EditorComponentImpl']") - fun documentMarkdownTableOps( - name: String, - directive: String, - out: PrintWriter, - file: File - ) { - val reportPrefix = "${name}_" - out.println("") - out.println( - """ - # $name - - In this demo, we add a table to a Markdown document, and then add columns and rows to the table. - - We start with this seed directive: - - ``` - $directive - Rows 1-5 - - | - ``` - - """.trimIndent() - ) - newFile("$name.md") - - click("//div[@class='EditorComponentImpl']") - keyboard.selectAll() - keyboard.key(KeyEvent.VK_DELETE) - enterLines( - """ - $directive - Rows 1-5 - - |""".trimIndent() - ) - keyboard.selectAll() - writeImage(menuAction("Append Text"), file, name, "${reportPrefix}menu", out) - awaitBackgroundProgress() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - This gives us the following Markdown document: - - ``` - """.trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) - writeImage(menuAction("Add Table Columns"), file, name, "${reportPrefix}add_columns", out) - awaitBackgroundProgress() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ```""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_Z) // Undo - writeImage(menuAction("Add Table Rows"), file, name, "${reportPrefix}add_rows", out) - awaitBackgroundProgress() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ```""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) - - // Close editor - sleep(1000) - click("//div[@class='InplaceButton']") - } - fun documentMarkdownListAppend( name: String, directive: String, @@ -855,7 +724,7 @@ class UITestUtil { getComponent(*path).click() } - fun clickr(path: String) { + private fun clickr(path: String) { getComponent(path).rightClick() } @@ -869,7 +738,7 @@ class UITestUtil { }() } - fun getComponent(vararg paths: String) = + private fun getComponent(vararg paths: String) = paths.flatMap { path -> try { listOf(robot.find(ComponentFixture::class.java, byXpath(path))) diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/actions/ActionTestBase.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/actions/ActionTestBase.kt index 95dedebe..aa29e7e7 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/actions/ActionTestBase.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/actions/ActionTestBase.kt @@ -12,10 +12,6 @@ import java.util.* open class ActionTestBase { companion object { - data class SelectionActionTestData( - val indent: String? = null, - ) - fun testScript_SelectionAction(selectionAction: SelectionAction, scriptPath: String) { AppSettingsState.instance.apiKey = OpenAIClient.keyTxt AppSettingsState.instance.temperature = 0.0 @@ -61,10 +57,6 @@ open class ActionTestBase { } } - data class FileContextActionTestData( - val filename: String? = null, - ) - inline fun testScript_FileContextAction( selectionAction: FileContextAction, scriptPath: String, diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClientTest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClientTest.kt deleted file mode 100644 index eddce41c..00000000 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClientTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.simiacryptus.aicoder.util - -class IdeaOpenAIClientTest { -// @Test -// fun testUiEdit() { -// IdeaOpenAIClient.uiEdit( -// //language=JSON -// project = null as Project?, -// jsonTxt = """ -// |{ -// | "key": "value" -// |} -// |""".trimMargin() -// ) -// } -}