diff --git a/README.md b/README.md index 18c77364..911bbb29 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.56 + 1.0.57 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.56' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.57' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.56") +implementation("com.simiacryptus:skyenet:1.0.57") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1a6adc93..1b6c69b7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -32,7 +32,7 @@ val jackson_version = "2.17.0" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.56") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.57") implementation("org.apache.commons:commons-text:1.11.0") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt index b32fe92c..a0231a6f 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt @@ -66,10 +66,8 @@ open class ActorSystem>( private fun getWrapper(name: String) = synchronized(wrapperMap) { wrapperMap.getOrPut(name) { FunctionWrapper(JsonFunctionRecorder( - File( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/actors/$name" - ).apply { mkdirs() })) + dataStorage.getSessionDir(user, session).resolve("actors/$name").apply { mkdirs() } + )) } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt index b84b49d3..5def9410 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ApplicationServices.kt @@ -7,7 +7,6 @@ import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.models.APIProvider import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.OpenAIModel -import com.simiacryptus.jopenai.util.JsonUtil import com.simiacryptus.skyenet.core.platform.file.* import com.simiacryptus.skyenet.core.util.Selenium import java.io.File @@ -127,7 +126,8 @@ interface StorageInterface { ): Date? fun listSessions( - user: User? + user: User?, + path: String, ): List fun setJson( @@ -144,7 +144,7 @@ interface StorageInterface { value: String ) - fun listSessions(dir: File): List + fun listSessions(dir: File, path: String): List fun userRoot(user: User?): File fun deleteSession(user: User?, session: Session) fun getMessageIds( diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt index 736479ba..ef34f112 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt @@ -118,7 +118,7 @@ open class ClientManager { null, user, OperationType.GlobalKey ) if (!canUseGlobalKey) throw RuntimeException("No API key") - val logfile = dataStorageRoot?.resolve("${if (session.isGlobal()) "global" else user}/$session/openai.log") + val logfile = dataStorageRoot?.resolve("${if (session.isGlobal()) "global" else "user-sessions/$user"}/$session/openai.log") ?.apply { parentFile?.mkdirs() } logfile?.parentFile?.mkdirs() return (if (ClientUtil.keyMap.isNotEmpty()) { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt index c39cc164..c590518a 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt @@ -1,7 +1,6 @@ package com.simiacryptus.skyenet.core.platform.file import com.simiacryptus.jopenai.util.JsonUtil -import com.simiacryptus.skyenet.core.platform.ApplicationServices import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.StorageInterface import com.simiacryptus.skyenet.core.platform.StorageInterface.Companion.validateSessionId @@ -9,6 +8,8 @@ import com.simiacryptus.skyenet.core.platform.User import java.io.File import java.text.SimpleDateFormat import java.util.* +import kotlin.reflect.jvm.javaType +import kotlin.reflect.typeOf open class DataStorage( private val dataDir: File @@ -21,7 +22,7 @@ open class DataStorage( validateSessionId(session) log.debug("Fetching messages for session: ${session.sessionId}, user: ${user?.email}") val messageDir = - ApplicationServices.dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/messages/") + getSessionDir(user, session).resolve("messages/") .apply { mkdirs() } val messages = LinkedHashMap() getMessageIds(user, session).forEach { messageId -> @@ -48,8 +49,8 @@ open class DataStorage( return when (parts.size) { 3 -> { val root = when { - parts[0] == "G" -> dataDir - parts[0] == "U" -> userRoot(user) + parts[0] == "G" -> dataDir.resolve("global") + parts[0] == "U" -> dataDir.resolve("user-sessions/$user") else -> throw IllegalArgumentException("Invalid session ID: $session") } val dateDir = File(root, parts[1]) @@ -59,8 +60,8 @@ open class DataStorage( } 2 -> { - val dateDir = File(dataDir, parts[0]) - val sessionDir = File(dateDir, parts[1]) + val dateDir = dataDir.resolve("global").resolve(parts[0]) + val sessionDir = dateDir.resolve(parts[1]) log.debug("Session directory for session: ${session.sessionId} is ${sessionDir.absolutePath}") sessionDir } @@ -105,10 +106,7 @@ open class DataStorage( log.debug("Fetching message IDs for session: ${session.sessionId}, user: ${user?.email}") val sessionDir = getSessionDir(user, session) val settings = run { - val settingsFile = File( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/internal.json" - ) + val settingsFile = sessionDir.resolve("internal.json") if (!settingsFile.exists()) null else { JsonUtil.objectMapper().readValue(settingsFile, Map::class.java) as Map<*, *> } @@ -117,8 +115,8 @@ open class DataStorage( val ids = messageFiles(session, user).entries.sortedBy { it.key.lastModified() } .map { it.key.nameWithoutExtension }.toList() setJson( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/internal.json", + sessionDir, + "internal.json", settings.plus("ids" to ids.joinToString(",")) ) log.debug("Message IDs for session: ${session.sessionId} are $ids") @@ -132,18 +130,16 @@ open class DataStorage( ) { validateSessionId(session) log.debug("Setting message IDs for session: ${session.sessionId}, user: ${user?.email} to $ids") + val sessionDir = getSessionDir(user, session) val settings = run { - val settingsFile = File( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/internal.json" - ) + val settingsFile = sessionDir.resolve("internal.json") if (!settingsFile.exists()) null else { JsonUtil.objectMapper().readValue(settingsFile, Map::class.java) as Map<*, *> } } ?: mapOf() setJson( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/internal.json", + sessionDir, + "internal.json", settings.plus("ids" to ids.joinToString(",")) ) } @@ -154,8 +150,8 @@ open class DataStorage( ): Date? { validateSessionId(session) log.debug("Fetching session time for session: ${session.sessionId}, user: ${user?.email}") - val settingsFile = - ApplicationServices.dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/internal.json") + val sessionDir = getSessionDir(user, session) + val settingsFile = sessionDir.resolve("internal.json") val settings = run { if (!settingsFile.exists()) null else { JsonUtil.objectMapper().readValue(settingsFile, Map::class.java) as Map<*, *> @@ -168,8 +164,8 @@ open class DataStorage( return if (null != file) { val date = Date(file.lastModified()) setJson( - ApplicationServices.dataStorageRoot, - "${if (session.isGlobal()) "global" else user}/$session/internal.json", + sessionDir, + "internal.json", settings.plus("time" to dateFormat.format(date)) ) log.debug("Session time for session: ${session.sessionId} is $date") @@ -183,8 +179,9 @@ open class DataStorage( private fun messageFiles( session: Session, user: User?, - ) = - ApplicationServices.dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/messages") + ): Map { + + return getSessionDir(user, session).resolve("messages") .apply { mkdirs() }.listFiles() ?.filter { file -> file.isFile } ?.map { messageFile -> @@ -203,13 +200,23 @@ open class DataStorage( } } }?.filter { it.second.isNotEmpty() }?.toList()?.toMap() ?: mapOf() + } override fun listSessions( - user: User? + user: User?, + path: String ): List { log.debug("Listing sessions for user: ${user?.email}") - val globalSessions = listSessions(dataDir) - val userSessions = if (user == null) listOf() else listSessions(userRoot(user)) + val globalSessions = listSessions(dataDir.resolve("global"), path) + val userSessions = if (user == null) listOf() else listSessions( + dataDir.resolve("user-sessions").resolve( + if (user?.email != null) { + user.email + } else { + throw IllegalArgumentException("User required for private session") + } + ).apply { mkdirs() }, path + ) log.debug("Found ${globalSessions.size} global sessions and ${userSessions.size} user sessions for user: ${user?.email}") return ((globalSessions.map { try { @@ -235,8 +242,7 @@ open class DataStorage( private fun setJson(sessionDir: File, filename: String, settings: T): T { log.debug("Setting JSON for session directory: ${sessionDir.absolutePath}, filename: $filename") - val settingsFile = File(sessionDir, filename) - settingsFile.parentFile.mkdirs() + val settingsFile = sessionDir.resolve(filename).apply { parentFile.mkdirs() } JsonUtil.objectMapper().writeValue(settingsFile, settings) return settings } @@ -250,7 +256,7 @@ open class DataStorage( validateSessionId(session) log.debug("Updating message for session: ${session.sessionId}, messageId: $messageId, user: ${user?.email}") val file = - ApplicationServices.dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/messages/$messageId.json") + getSessionDir(user, session).resolve("messages/$messageId.json") .apply { parentFile.mkdirs() } if (!file.exists()) { file.parentFile.mkdirs() @@ -270,32 +276,28 @@ open class DataStorage( } } - override fun listSessions(dir: File): List { + override fun listSessions(dir: File, path: String): List { log.debug("Listing sessions in directory: ${dir.absolutePath}") - val files = dir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir -> - val operationDir = File(sessionDir, "messages") - if (!operationDir.exists()) false else { - val listFiles = operationDir.listFiles()?.flatMap { - when { - it.isDirectory -> it.listFiles()?.toList() ?: listOf() - else -> listOf(it) - } - }?.filter { it.isFile && !it.name.startsWith("aaa") } - (listFiles?.size ?: 0) > 0 - } - }?.sortedBy { it.lastModified() } ?: listOf() + val files = dir.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: listOf() } + ?.filter { sessionDir -> + val resolve = sessionDir.resolve("info.json") + if (!resolve.exists()) return@filter false + val infoJson = resolve.readText() + val infoData = JsonUtil.fromJson>(infoJson, typeOf>().javaType) + path == infoData["path"] + }?.sortedBy { it.lastModified() } ?: listOf() log.debug("Found ${files.size} sessions in directory: ${dir.absolutePath}") return files.map { it.parentFile.name + "-" + it.name } } - override fun userRoot(user: User?) = File( - File(dataDir, "users"), + override fun userRoot(user: User?) = dataDir.resolve("users").resolve( if (user?.email != null) { user.email } else { throw IllegalArgumentException("User required for private session") } - ) + ).apply { mkdirs() } override fun deleteSession(user: User?, session: Session) { validateSessionId(session) diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/StorageInterfaceTest.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/StorageInterfaceTest.kt index 3e6a7fc8..30bf2640 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/StorageInterfaceTest.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/StorageInterfaceTest.kt @@ -92,7 +92,7 @@ abstract class StorageInterfaceTest(val storage: StorageInterface) { val user = User(email = "test@example.com") // Act - val sessions = storage.listSessions(user) + val sessions = storage.listSessions(user, "",) // Assert Assertions.assertNotNull(sessions) @@ -138,7 +138,7 @@ abstract class StorageInterfaceTest(val storage: StorageInterface) { val directory = File(System.getProperty("user.dir")) // Example directory // Act - val sessionList = storage.listSessions(directory) + val sessionList = storage.listSessions(directory, "") // Assert Assertions.assertNotNull(sessionList) diff --git a/gradle.properties b/gradle.properties index 412b08af..4bfee4ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup = com.simiacryptus.skyenet -libraryVersion = 1.0.72 +libraryVersion = 1.0.73 gradleVersion = 7.6.1 diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index c71b98da..3bf5d7fa 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -35,7 +35,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.17.0" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.56") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.57") implementation(project(":core")) implementation(project(":kotlin")) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt index d2c8809d..e7b98f29 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt @@ -1,6 +1,7 @@ package com.simiacryptus.skyenet import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import java.util.UUID object AgentPatterns { @@ -12,7 +13,7 @@ object AgentPatterns { val tasks = map.entries.map { (key, value) -> key to ui.newTask(root = false) }.toMap() - ui.socketManager.scheduledThreadPoolExecutor.schedule({ + ui.socketManager?.scheduledThreadPoolExecutor?.schedule({ tasks.forEach { (key, task) -> task.complete(map[key]!!) } @@ -20,7 +21,7 @@ object AgentPatterns { displayMapInTabs(tasks.mapValues { it.value.placeholder }, ui = ui, split = false) } else { """ - |
+ |
|
|${ map.keys.joinToString("\n") { key -> diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt index d07b02a0..937aef65 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt @@ -1,6 +1,7 @@ package com.simiacryptus.skyenet import com.simiacryptus.skyenet.webui.session.SessionTask +import java.util.* open class TabbedDisplay( val task: SessionTask, @@ -14,7 +15,7 @@ open class TabbedDisplay( val size: Int get() = tabs.size open fun render() = """ -
+
${renderTabButtons()} ${ tabs.withIndex().joinToString("\n") diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt index 87e35b59..15e2e7b0 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt @@ -83,8 +83,8 @@ open class CodingAgent( task.complete(newTask.placeholder) Retryable(ui, newTask) { val newTask = ui.newTask(root = false) - ui.socketManager.scheduledThreadPoolExecutor.schedule({ - ui.socketManager.pool.submit { + ui.socketManager?.scheduledThreadPoolExecutor!!.schedule({ + ui.socketManager?.pool?.submit { val statusSB = newTask.add("Running...") displayCode(newTask, codeRequest) statusSB?.clear() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt index b175e9ac..eb630f2b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt @@ -365,7 +365,7 @@ class WebDevAgent( }, outputFn = { code -> renderMarkdown( - ui.socketManager.addApplyFileDiffLinks( + ui.socketManager!!.addApplyFileDiffLinks( root = root.toPath(), code = { codeFiles.filter { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt index 58589c93..6aa7f415 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt @@ -6,7 +6,10 @@ import com.simiacryptus.skyenet.webui.session.SocketManagerBase import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Consumer -open class ApplicationInterface(val socketManager: SocketManagerBase) { +open class ApplicationInterface(val socketManager: SocketManagerBase?) { + + open fun isInteractive() = true + @Description("Returns html for a link that will trigger the given handler when clicked.") open fun hrefLink( @Description("The text to display in the link") @@ -17,19 +20,19 @@ open class ApplicationInterface(val socketManager: SocketManagerBase) { id: String? = null, @Description("The handler to trigger when the link is clicked") handler: Consumer, - ) = socketManager.hrefLink(linkText, classname, id, oneAtATime(handler)) + ) = socketManager!!.hrefLink(linkText, classname, id, oneAtATime(handler)) @Description("Returns html for a text input form that will trigger the given handler when submitted.") open fun textInput( @Description("The handler to trigger when the form is submitted") handler: Consumer - ): String = socketManager.textInput(oneAtATime(handler)) + ): String = socketManager!!.textInput(oneAtATime(handler)) @Description("Creates a new 'task' that can be used to display the progress of a long-running operation.") open fun newTask( //cancelable: Boolean = false root: Boolean = true - ): SessionTask = socketManager.newTask(cancelable = false, root = root) + ): SessionTask = socketManager!!.newTask(cancelable = false, root = root) companion object { fun oneAtATime(handler: Consumer): Consumer { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt index 60902e12..141d7e0c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt @@ -51,8 +51,14 @@ abstract class ApplicationServer( protected open val deleteSessionServlet by lazy { ServletHolder("delete", DeleteSessionServlet(this)) } protected open val cancelSessionServlet by lazy { ServletHolder("cancel", CancelThreadsServlet(this)) } - override fun newSession(user: User?, session: Session): SocketManager = - object : ApplicationSocketManager( + override fun newSession(user: User?, session: Session): SocketManager { + dataStorage.setJson(user, session, "info.json", mapOf( + "session" to session.toString(), + "application" to applicationName, + "path" to path, + "startTime" to System.currentTimeMillis(), + )) + return object : ApplicationSocketManager( session = session, owner = user, dataStorage = dataStorage, @@ -72,6 +78,7 @@ abstract class ApplicationServer( api = api ) } + } open fun userMessage( session: Session, @@ -109,7 +116,7 @@ abstract class ApplicationServer( userId: User? ): File { val settingsFile = - ApplicationServices.dataStorageRoot.resolve("${if (session.isGlobal()) "global" else userId}/$session/settings.json") + dataStorage.getSessionDir(userId, session).resolve("settings.json") .apply { parentFile.mkdirs() } return settingsFile } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt index d4fa3d58..9b0550fe 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt @@ -19,7 +19,7 @@ class SessionListServlet( resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK val user = authenticationManager.getUser(req.getCookie()) - val sessions = dataStorage.listSessions(user) + val sessions = dataStorage.listSessions(user, req.contextPath) val sessionRows = sessions.joinToString("") { session -> val sessionName = dataStorage.getSessionName(user, session) val sessionTime = dataStorage.getSessionTime(user, session) ?: return@joinToString "" diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionShareServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionShareServlet.kt index 40dd650d..959a3c1d 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionShareServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionShareServlet.kt @@ -46,12 +46,11 @@ class SessionShareServlet( require(acceptHost(user, host)) { "Invalid url: $url" } - val storageInterface = ApplicationServices.dataStorageFactory.invoke(File(dataStorageRoot, appName)) + val storageInterface = ApplicationServices.dataStorageFactory.invoke(dataStorageRoot) val session = StorageInterface.parseSessionID(sessionID) val pool = ApplicationServices.clientManager.getPool(session, user, server.dataStorage) - val json = JsonUtil.fromJson>( - dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/info.json") - .apply { parentFile.mkdirs() }.readText(), typeOf>().javaType) + val infoFile = storageInterface.getSessionDir(user, session).resolve("info.json").apply { parentFile.mkdirs() } + val json = if(infoFile.exists()) JsonUtil.fromJson>(infoFile.readText(), typeOf>().javaType) else mapOf() val sessionSettings = (json as? Map)?.toMutableMap() ?: mutableMapOf() val previousShare = sessionSettings["shareId"] when { @@ -92,12 +91,7 @@ class SessionShareServlet( try { log.info("Generating shareId: $shareId") sessionSettings["shareId"] = shareId - dataStorageRoot.resolve("${if (session.isGlobal()) "global" else user}/$session/info.json") - .apply { parentFile.mkdirs() }.writeText(JsonUtil.toJson(sessionSettings)) -// val selenium2S3 = Selenium2S3( -// pool = pool, -// cookies = cookies, -// ) + infoFile.writeText(JsonUtil.toJson(sessionSettings)) val selenium2S3: Selenium = ApplicationServices.seleniumFactory?.invoke(pool, cookies)!! if (selenium2S3 is Selenium2S3) { selenium2S3.loadImages = req.getParameter("loadImages")?.toBoolean() ?: false diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt index f01854cd..567e2732 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt @@ -15,6 +15,8 @@ abstract class SessionTask( private val spinner: String = SessionTask.spinner ) { + open fun isInteractive() = true + val placeholder: String get() = "
" private val currentText: String diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt index bbad6cd1..04e85347 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt @@ -26,7 +26,7 @@ abstract class SocketManagerBase( private val sendQueues: MutableMap> = mutableMapOf() private val messageVersions = HashMap() val pool get() = clientManager.getPool(session, owner, dataStorage) - val scheduledThreadPoolExecutor get() = clientManager.getScheduledPool(session, owner, dataStorage) + val scheduledThreadPoolExecutor get() = clientManager.getScheduledPool(session, owner, dataStorage)!! val sendQueue = ConcurrentLinkedDeque() override fun removeSocket(socket: ChatSocket) { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt index 1c95b36b..1efc4bb6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/util/MarkdownUtil.kt @@ -8,6 +8,7 @@ import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.data.MutableDataSet import org.apache.commons.text.StringEscapeUtils import java.nio.file.Files +import java.util.* object MarkdownUtil { fun renderMarkdown( @@ -43,7 +44,7 @@ object MarkdownUtil { log.warn("Failed to render Mermaid diagram", e) } val replacement = if (tabs) """ - |
+ |
|
| | diff --git a/webui/src/main/resources/application/functions.js b/webui/src/main/resources/application/functions.js index 681386e3..034f5664 100644 --- a/webui/src/main/resources/application/functions.js +++ b/webui/src/main/resources/application/functions.js @@ -215,7 +215,6 @@ function findAncestor(element, selector) { function applyToAllSvg() { - console.log("Applying SvgPanZoom to all SVG elements"); document.querySelectorAll('svg').forEach(svg => { if (!svg.dataset.svgPanZoomInitialized) { new SvgPanZoom().init(svg); diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index f36373c6..9a60a0f0 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -4,9 +4,10 @@ let singleInput = false; let stickyInput = false; let loadImages = "true"; let showMenubar = true; +let messageDiv; function onWebSocketText(event) { - console.log('WebSocket message:', event); + console.debug('WebSocket message:', event); const messagesDiv = document.getElementById('messages'); if (!messagesDiv) return; const firstCommaIndex = event.data.indexOf(','); @@ -14,14 +15,8 @@ function onWebSocketText(event) { const messageId = event.data.substring(0, firstCommaIndex); const messageVersion = event.data.substring(firstCommaIndex + 1, secondCommaIndex); const messageContent = event.data.substring(secondCommaIndex + 1); - // if (messageVersion <= (messageVersions[messageId] || 0)) { - // console.log("Ignoring message with id " + messageId + " and version " + messageVersion); - // return; - // } else { messageVersions[messageId] = messageVersion; messageMap[messageId] = messageContent; - // } - // Cleanup: remove temporary event listeners const messageDivs = document.querySelectorAll('[id="' + messageId + '"]'); messageDivs.forEach((messageDiv) => { @@ -88,11 +83,6 @@ function onWebSocketText(event) { } catch (e) { console.log("Error updating tabs: " + e); } - - /* If appPostMessage is defined, call it */ - if (typeof appPostMessage !== 'undefined') { - appPostMessage(event.data); - } } document.addEventListener('DOMContentLoaded', () => { @@ -104,6 +94,33 @@ document.addEventListener('DOMContentLoaded', () => { applyToAllSvg(); }, 5000); // Adjust the interval as needed + // Restore the selected tabs from localStorage before adding event listeners + document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.classList.add('active'); + const forTab = savedButton.getAttribute('data-for-tab'); + const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); + if (selectedContent) { + selectedContent.classList.add('active'); + selectedContent.style.display = 'block'; + } + console.log(`Restored saved tab: ${savedTab}`); + } + } + }); + document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.click(); + console.log(`Restored saved tab: ${savedTab}`); + } + } + }); const historyElement = document.getElementById('history'); if (historyElement) historyElement.addEventListener('click', () => showModal('sessions')); @@ -318,4 +335,16 @@ document.addEventListener('DOMContentLoaded', () => { }); updateTabs(); + + // Restore the selected tabs from localStorage + document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.click(); + console.log(`Restored saved tab: ${savedTab}`); + } + } + }); }); \ No newline at end of file diff --git a/webui/src/main/resources/application/tabs.js b/webui/src/main/resources/application/tabs.js index e8bc462a..b3c7d079 100644 --- a/webui/src/main/resources/application/tabs.js +++ b/webui/src/main/resources/application/tabs.js @@ -1,38 +1,54 @@ function updateTabs() { document.querySelectorAll('.tab-button').forEach(button => { - button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter + // console.log(`Adding click event listener to tab button: ${button.getAttribute('data-for-tab')}`); + button.addEventListener('click', (event) => { + // console.log(`Tab button clicked: ${button.getAttribute('data-for-tab')}`); event.stopPropagation(); const forTab = button.getAttribute('data-for-tab'); + const tabsContainerId = button.closest('.tabs-container').id; + // console.log(`Tabs container ID: ${tabsContainerId}`); + // console.log(`Saving selected tab to localStorage: selectedTab_${tabsContainerId} = ${forTab}`); + localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); let tabsParent = button.closest('.tabs-container'); tabsParent.querySelectorAll('.tab-button').forEach(tabButton => { if (tabButton.closest('.tabs-container') === tabsParent) tabButton.classList.remove('active'); }); button.classList.add('active'); + // console.log(`Active tab set to: ${forTab}`); let selectedContent = null; tabsParent.querySelectorAll('.tab-content').forEach(content => { if (content.closest('.tabs-container') === tabsParent) { if (content.getAttribute('data-tab') === forTab) { content.classList.add('active'); content.style.display = 'block'; // Ensure the content is displayed + // console.log(`Content displayed for tab: ${forTab}`); selectedContent = content; } else { content.classList.remove('active'); content.style.display = 'none'; // Ensure the content is hidden + // console.log(`Content hidden for tab: ${content.getAttribute('data-tab')}`); } } }); if (selectedContent !== null) updateNestedTabs(selectedContent); }); + // Check if the current button should be activated based on localStorage + const savedTab = localStorage.getItem(`selectedTab_${button.closest('.tabs-container').id}`); + if (button.getAttribute('data-for-tab') === savedTab) { + button.dispatchEvent(new Event('click')); + } }); } function updateNestedTabs(element) { element.querySelectorAll('.tabs-container').forEach(tabsContainer => { try { + // console.log(`Updating nested tabs for container: ${tabsContainer.id}`); let hasActiveButton = false; tabsContainer.querySelectorAll('.tab-button').forEach(nestedButton => { if (nestedButton.classList.contains('active')) { hasActiveButton = true; + // console.log(`Found active nested button: ${nestedButton.getAttribute('data-for-tab')}`); } }); if (!hasActiveButton) { @@ -43,24 +59,43 @@ function updateNestedTabs(element) { const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); if (activeButton !== null) { activeButton.classList.add('active'); + // console.log(`Set active nested button: ${activeTab}`); } } else { /* Add 'active' to the class list of the first button */ const firstButton = tabsContainer.querySelector('.tab-button'); if (firstButton !== null) { firstButton.classList.add('active'); + // console.log(`Set first nested button as active: ${firstButton.getAttribute('data-for-tab')}`); } } } + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + // console.log(`Retrieved saved tab from localStorage: selectedTab_${tabsContainer.id} = ${savedTab}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.classList.add('active'); + const forTab = savedButton.getAttribute('data-for-tab'); + const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); + if (selectedContent) { + selectedContent.classList.add('active'); + selectedContent.style.display = 'block'; + } + // console.log(`Restored saved tab: ${savedTab}`); + } + } } catch (e) { - console.log("Error updating tabs: " + e); + // console.log("Error updating tabs: " + e); } }); } document.addEventListener('DOMContentLoaded', () => { + // console.log('Document loaded. Initializing tabs...'); updateTabs(); updateNestedTabs(document); + }); window.updateTabs = updateTabs; // Expose updateTabs to the global scope \ No newline at end of file diff --git a/webui/src/main/resources/welcome/functions.js b/webui/src/main/resources/welcome/functions.js index 14cf4dfe..69028783 100644 --- a/webui/src/main/resources/welcome/functions.js +++ b/webui/src/main/resources/welcome/functions.js @@ -191,7 +191,6 @@ function findAncestor(element, selector) { function applyToAllSvg() { - console.log("Applying SvgPanZoom to all SVG elements"); document.querySelectorAll('svg').forEach(svg => { if (!svg.dataset.svgPanZoomInitialized) { new SvgPanZoom().init(svg); diff --git a/webui/src/main/resources/welcome/tabs.js b/webui/src/main/resources/welcome/tabs.js index e8bc462a..b3c7d079 100644 --- a/webui/src/main/resources/welcome/tabs.js +++ b/webui/src/main/resources/welcome/tabs.js @@ -1,38 +1,54 @@ function updateTabs() { document.querySelectorAll('.tab-button').forEach(button => { - button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter + // console.log(`Adding click event listener to tab button: ${button.getAttribute('data-for-tab')}`); + button.addEventListener('click', (event) => { + // console.log(`Tab button clicked: ${button.getAttribute('data-for-tab')}`); event.stopPropagation(); const forTab = button.getAttribute('data-for-tab'); + const tabsContainerId = button.closest('.tabs-container').id; + // console.log(`Tabs container ID: ${tabsContainerId}`); + // console.log(`Saving selected tab to localStorage: selectedTab_${tabsContainerId} = ${forTab}`); + localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); let tabsParent = button.closest('.tabs-container'); tabsParent.querySelectorAll('.tab-button').forEach(tabButton => { if (tabButton.closest('.tabs-container') === tabsParent) tabButton.classList.remove('active'); }); button.classList.add('active'); + // console.log(`Active tab set to: ${forTab}`); let selectedContent = null; tabsParent.querySelectorAll('.tab-content').forEach(content => { if (content.closest('.tabs-container') === tabsParent) { if (content.getAttribute('data-tab') === forTab) { content.classList.add('active'); content.style.display = 'block'; // Ensure the content is displayed + // console.log(`Content displayed for tab: ${forTab}`); selectedContent = content; } else { content.classList.remove('active'); content.style.display = 'none'; // Ensure the content is hidden + // console.log(`Content hidden for tab: ${content.getAttribute('data-tab')}`); } } }); if (selectedContent !== null) updateNestedTabs(selectedContent); }); + // Check if the current button should be activated based on localStorage + const savedTab = localStorage.getItem(`selectedTab_${button.closest('.tabs-container').id}`); + if (button.getAttribute('data-for-tab') === savedTab) { + button.dispatchEvent(new Event('click')); + } }); } function updateNestedTabs(element) { element.querySelectorAll('.tabs-container').forEach(tabsContainer => { try { + // console.log(`Updating nested tabs for container: ${tabsContainer.id}`); let hasActiveButton = false; tabsContainer.querySelectorAll('.tab-button').forEach(nestedButton => { if (nestedButton.classList.contains('active')) { hasActiveButton = true; + // console.log(`Found active nested button: ${nestedButton.getAttribute('data-for-tab')}`); } }); if (!hasActiveButton) { @@ -43,24 +59,43 @@ function updateNestedTabs(element) { const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); if (activeButton !== null) { activeButton.classList.add('active'); + // console.log(`Set active nested button: ${activeTab}`); } } else { /* Add 'active' to the class list of the first button */ const firstButton = tabsContainer.querySelector('.tab-button'); if (firstButton !== null) { firstButton.classList.add('active'); + // console.log(`Set first nested button as active: ${firstButton.getAttribute('data-for-tab')}`); } } } + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + // console.log(`Retrieved saved tab from localStorage: selectedTab_${tabsContainer.id} = ${savedTab}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.classList.add('active'); + const forTab = savedButton.getAttribute('data-for-tab'); + const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); + if (selectedContent) { + selectedContent.classList.add('active'); + selectedContent.style.display = 'block'; + } + // console.log(`Restored saved tab: ${savedTab}`); + } + } } catch (e) { - console.log("Error updating tabs: " + e); + // console.log("Error updating tabs: " + e); } }); } document.addEventListener('DOMContentLoaded', () => { + // console.log('Document loaded. Initializing tabs...'); updateTabs(); updateNestedTabs(document); + }); window.updateTabs = updateTabs; // Expose updateTabs to the global scope \ No newline at end of file