diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9f72e0c8..28ea1095 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.53") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.54") implementation("org.apache.commons:commons-text:1.11.0") diff --git a/docs/tabbed_ui.md b/docs/tabbed_ui.md new file mode 100644 index 00000000..d1ad3f37 --- /dev/null +++ b/docs/tabbed_ui.md @@ -0,0 +1,190 @@ +# Tabbed UI Documentation + +## Overview + +The tabbed UI components in the codebase are specifically designed to facilitate the creation, management, and seamless +integration of tabbed interfaces within web applications. Ideal for applications requiring dynamic content switching +without page reloads, these components support various content types and are driven by user interactions or backend +processes. They are particularly useful in dashboards, multi-step forms, and settings panels. + +## Components + +### TabbedDisplay + +`TabbedDisplay` is a base class for creating tabbed displays. It manages a list of tabs, each associated with a label +and content. It provides methods to render tabs, manage tab content, and handle user interactions to switch between +tabs. + +#### Key Methods and Properties + +### Retryable + +`Retryable` extends `TabbedDisplay` to add functionality for retrying operations. It is particularly useful for +operations that might fail and need a retry mechanism, encapsulated within a tab. + +#### Key Methods + +### AgentPatterns + +`AgentPatterns` contains utility methods for displaying content in a tabbed format without directly managing the tab +state. + +#### Key Functions + +a tabbed interface, optionally splitting content into separate tasks for performance. + +### MarkdownUtil + +`MarkdownUtil` provides utilities for rendering Markdown content into HTML, including special handling for Mermaid +diagrams and tabbed displays of Markdown versus rendered HTML. + +#### Key Methods + +Markdown into HTML, with support for Mermaid diagrams and an optional tabbed interface. + +## Usage Examples + +### Example 1: Basic Tabbed Display + +```kotlin +val tabbedDisplay = TabbedDisplay(sessionTask) +tabbedDisplay.set("Tab 1", "Content for Tab 1") +tabbedDisplay.set("Tab 2", "Content for Tab 2") +tabbedDisplay.update() +``` + +This example demonstrates how to create a simple tabbed interface with two tabs. + +Detailed examples of using these components can be added here, including code snippets and explanations of how to +integrate the tabbed UI into a web application. + +## Conclusion + +The tabbed UI components are versatile tools for building interactive and dynamic web interfaces. By understanding and +utilizing these components, developers can enhance the user experience of their applications. + +### Tabs + +The `TabbedDisplay` class in `TabbedDisplay.kt` implements a tabbed UI component that allows displaying multiple pieces +of content in a tabbed interface. + +#### High Level Design + +- The `TabbedDisplay` class maintains a list of tab name and content `StringBuilder` pairs. +- It provides methods to get/set tab content by name, find tabs by name, and update the rendered HTML. +- The `render()` method generates the HTML for the tabbed interface, including tab buttons and content divs. +- An instance keeps a reference to the `container` `StringBuilder` it is rendered into, allowing it to update itself. + +#### Usage + +To use the `TabbedDisplay` class: + +1. Create an instance, passing the `SessionTask` it is associated with +2. Add tabs using the `set(name: String, content: String)` method. This will create the tab if it doesn't exist or + update its content if it does. +3. Retrieve tab content using the `get(i: String)` method +4. Call `update()` after modifying tabs to re-render the component HTML + +The tabbed content will automatically be displayed in the associated `SessionTask`. + +### Retry + +The `Retryable` class in `Retryable.kt` extends `TabbedDisplay` to add a "retry" button that re-runs a block of code and +adds the output as a new tab. + +#### High Level Design + +`Retryable` overrides the `renderTabButtons()` method to add a recycle ♻ button after the tab buttons. This button, when +clicked, triggers the retry mechanism. + +- When clicked, the retry callback: + 1. Adds a new tab with a "Retrying..." placeholder and calls `update()` + 2. Runs the `process` lambda passed to the constructor, passing a `StringBuilder` for output + 3. Replaces the placeholder content with the final output +- This allows easily re-running a block of code and capturing the new output in a new tab + +#### Usage + +To use `Retryable`: + +1. Create an instance, passing the `ApplicationInterface`, `SessionTask` and retry process lambda +2. The retry button will automatically be shown and will run the `process` lambda when clicked +3. The `process` lambda should return the `String` content to display in the new tab + +### Example 2: Using Retryable + + ```kotlin + val applicationInterface = getApplicationInterface() // Assume this returns an ApplicationInterface instance +val sessionTask = getSessionTask() // Assume this returns a SessionTask instance +val retryable = Retryable(applicationInterface, sessionTask) { stringBuilder -> + try { + // Code that might fail and needs retrying + "Operation successful" + } catch (e: Exception) { + stringBuilder.append("Error encountered: ${e.message}") + "Retry failed" + } +} +retryable.update() + ``` + +This example demonstrates how to use `Retryable` to add retry functionality. The lambda function provided to `Retryable` +is executed when the retry button is clicked. If the operation is successful, it returns a success message; otherwise, +it logs the error and returns a failure message. +By using `Retryable`, you can add retry functionality to a tabbed display with just a few lines of code. + +### Acceptable + +The `Acceptable` class is designed to handle user interactions that require acceptance or feedback before proceeding. It +extends the functionality of `TabbedDisplay` by integrating user input and decision-making processes directly into the +tabbed interface. + +#### High Level Design + +`Acceptable` manages a sequence of user interactions within a tabbed display, where each tab can represent a stage in a +decision-making process. It uses a combination of user prompts, text inputs, and acceptance links to gather and process +user feedback. + +- The class initializes with a user message and a function to process the initial response. +- It dynamically adds tabs based on user interactions and updates the display accordingly. +- A feedback mechanism allows users to revise their responses, which the system processes to potentially alter the + subsequent flow or decisions. + +#### Key Methods + +- `main()`: Orchestrates the initial display and subsequent updates based on user interactions. +- `feedbackForm()`: Generates the HTML form for user feedback within the tab. +- `acceptLink()`: Provides a link for the user to confirm their decision, moving the process to the next stage. + +#### Usage + +To use `Acceptable`: + +1. Instantiate `Acceptable` with necessary parameters like the session task, user message, initial response processing + function, and UI interface. +2. Use the `call()` method to start the interaction process and wait for the user's final acceptance. +3. The class handles user inputs and updates the tabbed display dynamically, reflecting the stages of user interaction + and decision-making. + +### Example 3: Using Acceptable + +```kotlin +val sessionTask = getSessionTask() // Assume this returns a SessionTask instance +val userMessage = "Please review the information and accept to proceed." +val acceptable = Acceptable( + task = sessionTask, + userMessage = userMessage, + initialResponse = { msg -> processInitialResponse(msg) }, + outputFn = { response -> response.toString() }, + ui = getApplicationInterface(), // Assume this returns an ApplicationInterface instance + reviseResponse = { history -> reviseUserResponse(history) }, + heading = "User Acceptance Required" +) +acceptable.call() +``` + +This example demonstrates how to use `Acceptable` to manage a user acceptance process within a tabbed interface. The +user is prompted to review information and provide feedback or accept to proceed, with each stage managed as a separate +tab. + +Let me know if you have any other questions! \ No newline at end of file diff --git a/docs/task_ui.md b/docs/task_ui.md new file mode 100644 index 00000000..864b0892 --- /dev/null +++ b/docs/task_ui.md @@ -0,0 +1,214 @@ +# Task UI + +The `SessionTask` class provides a way to display long-running tasks with progress updates in the UI. It allows you to +append messages, headers, errors, and images to the task output. + +## Methods + +### add(message: String, showSpinner: Boolean = true, tag: String = "div", className: String = "response-message") + +Adds a message to the task output. The message will be wrapped in the specified HTML tag with the given CSS class. +If `showSpinner` is true, a loading spinner will be displayed after the message to indicate ongoing processing. +The `tag` parameter allows customization of the HTML tag used, and `className` specifies the CSS class for styling. + +### hideable(ui: ApplicationInterface?, message: String, showSpinner: Boolean = true, tag: String = "div", className: String = "response-message"): StringBuilder? + +Adds a hideable message to the task output. The message will include a close button, allowing the user to manually hide +the message. This method returns a `StringBuilder` instance containing the message, which can be manipulated further if +needed. The `ui` parameter is used to handle UI interactions for the close button. + +### echo(message: String, showSpinner: Boolean = true, tag: String = "div") + +Echos a user message to the task output. +This method is typically used for echoing user inputs or commands back to the UI for confirmation or logging purposes. + +### header(message: String, showSpinner: Boolean = true, tag: String = "div", classname: String = "response-header") + +Adds a header to the task output with the specified CSS class. +Headers are useful for separating sections of output or introducing new stages of task progress. + +### verbose(message: String, showSpinner: Boolean = true, tag: String = "pre") + +Adds a verbose message to the task output. Verbose messages are hidden by default and wrapped in a `
` tag.
+This method is ideal for displaying detailed diagnostic or debug information that can be expanded or collapsed by the
+user.
+
+### error(ui: ApplicationInterface?, e: Throwable, showSpinner: Boolean = false, tag: String = "div")
+
+Displays an error in the task output. This method is specialized to handle different types of exceptions:
+
+- `ValidationError`: Displays a message indicating a validation error along with a detailed stack trace.
+- `FailedToImplementException`: Shows a message indicating a failure in implementation, including relevant code snippets
+  and language details.
+- Other exceptions: Displays the exception name and a complete stack trace to aid in debugging. The `showSpinner`
+  parameter can be set to `false` to not show a spinner, as errors typically denote the end of processing.
+
+### complete(message: String = "", tag: String = "div", className: String = "response-message")
+
+Displays a final message in the task output and hides the spinner, indicating that the task has been completed. If no
+message is provided, only the spinner will be hidden without any additional text.
+
+### Placeholder Mechanism
+ 
+Each `SessionTask` instance generates a unique placeholder in the UI, represented by an HTML `div` element with an `id`
+attribute set to the task's `operationID`. This placeholder serves as a dynamic container where task-related updates and
+outputs are rendered in real-time. This mechanism is crucial for maintaining a responsive and interactive user interface
+during the execution of tasks.
+
+### Non-Root Tasks
+
+When creating a new task with the `newTask(root: Boolean)` method, setting `root` to `false` allows the creation of a
+subordinate task. Non-root tasks are typically used for operations that are part of a larger task or workflow. They
+inherit the context and permissions of the parent task, enabling structured and hierarchical task management within the
+application.
+
+### image(image: BufferedImage)
+
+Displays an image in the task output. The image is saved as a PNG file, and the URL of the saved image is embedded
+within an `` tag to be displayed in the UI.
+
+## Saving Files
+
+The `saveFile(relativePath: String, data: ByteArray): String` method allows saving file data and returns the URL of the
+saved file. This is useful for displaying images or providing file downloads in the task output.
+This method is crucial for managing file outputs in tasks that involve file generation or manipulation, ensuring that
+users can access or download the generated files directly from the task UI.
+
+## Overview of the Task UI API
+
+The Task UI API in the provided Kotlin codebase is designed to facilitate the creation and management of user interface
+tasks within a web application. This API is part of a larger system that likely involves real-time interaction with
+users through a web interface. The main components involved in the Task UI API
+include `SessionTask`, `ApplicationInterface`, and utility functions and classes that support task management and
+display.
+The API's design focuses on providing a seamless and dynamic user experience, where tasks can be monitored and
+controlled interactively, enhancing the overall user engagement and efficiency of the web application.
+
+### Key Components
+
+#### 1. `SessionTask`
+
+`SessionTask` is an abstract class that represents a task session in the UI. It is designed to handle the dynamic output
+of content to the user interface during the execution of a task. Key functionalities include:
+
+- **Progress Tracking**: Allows real-time tracking of task progress through various states, providing immediate feedback
+  to the user.
+- **Interactive Elements**: Supports adding interactive elements like buttons and links within the task output, enabling
+  user actions directly from the task interface.
+
+- **Dynamic Content Management**: It manages a buffer that aggregates output content which can be dynamically updated
+  and displayed in the UI.
+- **Abstract Methods**:
+    - `send(html: String)`: Sends the compiled HTML content to the UI.
+    - `saveFile(relativePath: String, data: ByteArray)`: Saves a file and returns a URL to access it, used for handling
+      file outputs like images.
+- **Utility Methods**:
+    - `add(message: String, showSpinner: Boolean, tag: String, className: String)`: Adds a message to the UI with
+      configurable HTML wrapping and CSS styling.
+    - `hideable(...)`, `echo(...)`, `header(...)`, `verbose(...)`, `error(...)`, `complete(...)`, `image(...)`: These
+      methods provide specialized ways to add different types of content to the UI, such as hideable messages, errors,
+      headers, and images.
+
+#### 2. `ApplicationInterface`
+
+`ApplicationInterface` serves as a bridge between the task management logic and the socket communication
+layer (`SocketManagerBase`). It provides methods to interact with the user through hyperlinks and form inputs, and to
+manage tasks:
+
+- **Dynamic Task Creation**: Dynamically creates tasks based on user interactions or automated triggers, ensuring that
+  each task is tailored to the specific needs of the operation.
+- **Enhanced User Interaction**: Facilitates richer user interaction models by providing utility methods for creating
+  interactive UI components like hyperlinks and text inputs.
+
+- **Task Creation**: `newTask(root: Boolean)`: Creates a new `SessionTask` instance.
+- **UI Element Creation**:
+    - `hrefLink(...)`: Generates HTML for a clickable link that triggers a specified handler.
+    - `textInput(...)`: Generates HTML for a text input form that triggers a specified handler upon submission.
+
+#### 3. Utility Functions and Classes
+
+- **`AgentPatterns`**: Contains utility functions like `displayMapInTabs(...)`, which helps in displaying data in a
+  tabbed interface.
+- **Error Handling**: Includes mechanisms to gracefully handle and display errors within the UI, ensuring that users are
+  well-informed about any issues during task execution.
+- **Session Management**: Provides robust session management capabilities to maintain the state and continuity of user
+  tasks.
+- **Image Handling**: The `toPng()` extension function for `BufferedImage` converts an image to PNG format, useful in
+  tasks that involve image processing.
+
+### Usage Example
+
+To use the Task UI API, a developer would typically instantiate a `SessionTask` through `ApplicationInterface` and use
+the task's methods to dynamically add content to the UI based on the application's logic and user interactions. For
+example:
+
+```kotlin
+val appInterface = ApplicationInterface(socketManager)
+val task = appInterface.newTask()
+task.header("Processing Data")
+// Perform some data processing
+task.add("Data processed successfully", showSpinner = false)
+task.complete("Task completed.")
+```
+
+### Conclusion
+
+The Task UI API provides a robust set of tools for managing interactive tasks in a web application. By abstracting the
+complexities of real-time UI updates and task management, it allows developers to focus on the core logic of their
+applications while providing a responsive and interactive user experience.
+
+The `SessionTask` class in the Kotlin codebase provides a mechanism to manage and display linked tasks in a web UI,
+particularly through the use of placeholders. This functionality is crucial for tasks that are interdependent or need to
+be executed in a sequence, allowing the UI to dynamically update as tasks progress or complete.
+
+## Placeholders
+
+The placeholder is a unique identifier used to represent a task in the UI dynamically. It allows the system to update
+the task's output in real-time without reloading the entire page. Here's how it is implemented and used:
+
+#### Placeholder Generation
+
+Each `SessionTask` instance has an `operationID`, which is a unique identifier for that task. The `placeholder` is an
+HTML `div` element with its `id` attribute set to the `operationID`. This `div` acts as a placeholder in the HTML
+document where the task's output will be dynamically inserted or updated.
+
+```kt
+val placeholder: String get() = "
" +``` + +#### Using the Placeholder + +When a new task is created, its placeholder is initially empty. As the task progresses, messages, errors, images, or +other outputs are dynamically inserted into this placeholder using JavaScript and WebSocket communication. This approach +allows the UI to remain responsive and update in real-time as the task outputs change. + +### Example of Placeholder Usage + +Here’s a simplified example to illustrate how placeholders are typically used in the system: + +1. **Task Creation**: A new `SessionTask` is instantiated, and its placeholder is added to the web page. + ```kt + val task = appInterface.newTask() + val placeholderHtml = task.placeholder + ``` + +2. **Task Execution**: The task performs its operations, during which it may use methods like `add`, `error`, + or `complete` to update its output. + ```kt + task.add("Processing data...") + // Some processing happens here + task.complete("Processing complete.") + ``` + +3. **Dynamic UI Update**: As the task updates its output, these changes are sent to the client's browser using WebSocket + messages. The JavaScript on the client side listens for these messages and updates the inner HTML of the `div` with + the corresponding `operationID`. + +4. **Final Output**: Once the task completes, the final message is displayed in the placeholder, and no further updates + occur unless a new task is linked or started. + +### Conclusion + +The placeholder mechanism in `SessionTask` is a powerful feature that supports dynamic and real-time updates to the web +UI without requiring page refreshes. It is especially useful in applications that involve complex or long-running tasks, +providing users with immediate feedback and enhancing the interactivity of the application. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ea8cddaa..f7c325e3 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.64 +libraryVersion = 1.0.65 gradleVersion = 7.6.1 diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index a9f3ba34..70c843ce 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.53") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.54") implementation(project(":core")) implementation(project(":kotlin")) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt index 4ff7111a..39f8efc7 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt @@ -22,48 +22,44 @@ class Acceptable( ) : Callable { val tabs = object : TabbedDisplay(task) { - override fun renderTabButtons(): String { - return """ -
- ${ - tabs.withIndex().joinToString("\n") - { (index: Int, t: Pair) -> - """""" - } + override fun renderTabButtons() = """ + |
+ |${ + tabs.withIndex().joinToString("\n") + { (index: Int, t: Pair) -> + """""" } - ${ - ui.hrefLink("♻") { - val idx: Int = size - set(label(idx), "Retrying...") - task.add("") - main(idx, this@Acceptable.task) - } - } -
- """.trimIndent() } + |${ + ui.hrefLink("♻") { + val idx: Int = size + val newTask = ui.newTask(false) + val header = newTask.header("Retrying...") + this[label(idx)] = newTask.placeholder + main(idx, newTask) + header?.clear() + newTask.complete() + } + } + |
+ """.trimMargin() } private val acceptGuard = AtomicBoolean(false) - fun main(tabIndex: Int = tabs.size, task: SessionTask = this.task) { + private fun main(tabIndex: Int, task: SessionTask) { try { val history = mutableListOf>() history.add(userMessage to Role.user) val design = initialResponse(userMessage) history.add(outputFn(design) to Role.assistant) - val tabLabel = tabs.label(tabIndex) - val tabContent = tabs[tabLabel] ?: tabs.set(tabLabel, "") - - if (tabs.size > tabIndex) { - tabContent.append(outputFn(design) + "\n" + feedbackForm(tabIndex, tabContent, design, history, task)) - } else { - tabContent.set(outputFn(design) + "\n" + feedbackForm(tabIndex, tabContent, design, history, task)) - } - tabs.update() + val tabContent = task.add(outputFn(design))!! + val feedbackForm = feedbackForm(tabIndex, tabContent, design, history, task) + tabContent?.append("\n" + feedbackForm.placeholder) + task.complete() } catch (e: Throwable) { task.error(ui, e) task.complete(ui.hrefLink("🔄 Retry") { - main(task = task) + main(tabIndex = tabIndex, task = task) }) } } @@ -74,20 +70,29 @@ class Acceptable( design: T, history: List>, task: SessionTask, - ): String = """ - | - |
- |${acceptLink(tabIndex, tabContent, design)} - |
- |${textInput(design, tabContent, history, task)} - | - """.trimMargin() + ) = ui.newTask(false).apply { + val feedbackSB = add("
")!! + feedbackSB.clear() + feedbackSB.append( + """ + |
+ |${acceptLink(tabIndex, tabContent, design, feedbackSB, feedbackTask = this)} + |
+ |${textInput(design, tabContent, history, task, feedbackSB, feedbackTask = this)} + """.trimMargin() + ) + complete() + } private fun acceptLink( tabIndex: Int?, tabContent: StringBuilder, design: T, + feedbackSB: StringBuilder, + feedbackTask: SessionTask, ) = ui.hrefLink("Accept", classname = "href-link cmd-button") { + feedbackSB.clear() + feedbackTask.complete() accept(tabIndex, tabContent, design) } @@ -96,14 +101,21 @@ class Acceptable( tabContent: StringBuilder, history: List>, task: SessionTask, + feedbackSB: StringBuilder, + feedbackTask: SessionTask, ): String { val feedbackGuard = AtomicBoolean(false) return ui.textInput { userResponse -> if (feedbackGuard.getAndSet(true)) return@textInput + val prev = feedbackSB.toString() try { + feedbackSB.clear() + feedbackTask.complete() feedback(tabContent, userResponse, history, design, task) } catch (e: Exception) { task.error(ui, e) + feedbackSB.set(prev) + feedbackTask.complete() throw e } finally { feedbackGuard.set(false) @@ -120,20 +132,18 @@ class Acceptable( ) { var history = history history = history + (userResponse to Role.user) - val prevValue = tabContent.toString() - val newValue = (prevValue.substringBefore("") - + "" - + prevValue.substringAfter("") - + "
" - + renderMarkdown(userResponse, ui = ui) - + "
") + val newValue = (tabContent.toString() + + "
" + + renderMarkdown(userResponse, ui = ui) + + "
") tabContent.set(newValue) - task.add("") // Show spinner + val stringBuilder = task.add("Processing...") tabs.update() val newDesign = reviseResponse(history) val newTask = ui.newTask(root = false) tabContent.set(newValue + "\n" + newTask.placeholder) tabs.update() + stringBuilder?.clear() task.complete() Retryable(ui, newTask) { outputFn(newDesign) + "\n" + feedbackForm( @@ -142,7 +152,7 @@ class Acceptable( design = newDesign, history = history, task = newTask - ) + ).placeholder }.apply { set(label(size), process(container)) } @@ -156,11 +166,7 @@ class Acceptable( if (null != tabIndex) tabs.selectedTab = tabIndex tabContent.apply { val prevTab = toString() - val newValue = - prevTab.substringBefore("") + "" + prevTab.substringAfter( - "" - ) - set(newValue) + set(prevTab) tabs.update() } } catch (e: Exception) { @@ -174,7 +180,13 @@ class Acceptable( override fun call(): T { task.echo(heading) - main() + val idx = tabs.size + val newTask = ui.newTask(false) + val header = newTask.header("Processing...") + tabs[tabs.label(idx)] = newTask.placeholder + main(idx, newTask) + header?.clear() + newTask.complete() semaphore.acquire() return atomicRef.get() } 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 ade6a6ec..de8c66b1 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 @@ -35,7 +35,7 @@ open class CodingAgent( temperature: Double = 0.1, val details: String? = null, val model: ChatModels, - private val mainTask: SessionTask = ui.newTask(), + private val mainTask: SessionTask, val actorMap: Map = mapOf( ActorTypes.CodingActor to CodingActor( interpreter, diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/interpreter/ProcessInterpreter.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/interpreter/ProcessInterpreter.kt index 2662ba1c..580b1279 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/interpreter/ProcessInterpreter.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/interpreter/ProcessInterpreter.kt @@ -1,6 +1,5 @@ package com.simiacryptus.skyenet.interpreter -import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit open class ProcessInterpreter(