diff --git a/docs/project_info.md b/docs/project_info.md new file mode 100644 index 00000000..02cade69 --- /dev/null +++ b/docs/project_info.md @@ -0,0 +1,209 @@ +# Software Project Formalization and Documentation Questionnaire + +## General Information + +1. **Project Name:** + Skyenet LLM Agent Platform + +2. **Project Description:** + The Skyenet LLM Agent Platform is designed to facilitate the integration and utilization of large language models (LLMs) in various applications. It provides a framework for managing LLM interactions, handling user inputs, and displaying dynamic content. + +3. **Project Objectives:** + * To create a robust and flexible platform that supports real-time LLM interactions. + * To provide a seamless user experience with dynamic content updates. + * To integrate various backend processes and user interactions using LLMs. + +4. **Target Audience:** + Developers building applications that leverage large language models for tasks such as coding assistance, image generation, text-to-speech conversion, and more. + +## Scope and Deliverables + +1. **Assumptions and Context:** + * What assumptions are being made about the project? + * The system will be used in applications that require real-time LLM interactions. + * Users will have access to modern LLM models and necessary computational resources. + * What is the context in which this project is being developed? + * The project is being developed to enhance user interaction in applications by providing a dynamic and responsive LLM-based platform. + * What problem is this project aiming to solve? + * The project aims to solve the problem of integrating and utilizing large language models in applications, by providing a flexible and robust LLM agent platform. + * The project aims to solve the problem of static web pages that require full page reloads for updates, by + providing a real-time, interactive UI framework. + +2. **Scope:** + The project includes the development of an LLM agent platform that supports real-time interactions using large language models, dynamic content updates, and integration with various backend processes. + +3. **Deliverables:** + * An LLM-based agent platform. + * Documentation for developers. + * Example implementations and usage guides. + * Integration with backend processes. + +4. **Out of Scope:** + * Development of LLM models. + * Support for outdated computational resources that cannot handle modern LLMs. + +5. **Use Case Details:** + * **Coding assistance**: Generate and execute code snippets based on natural language instructions. + * **Image generation**: Transform textual descriptions into images using image generation models. + * **Text-to-speech conversion**: Convert text into speech using text-to-speech models. + * **Interactive discussions**: Facilitate user interactions and collect feedback to refine responses. + +## Developer Documentation + +1. **Code Repository:** + The code repository will be hosted on GitHub at https://github.com/your-repo/skyenet-llm-agent-platform + + * Repository URL: + * Branching strategy: + * Contribution guidelines: + +2. **Development Setup:** + * Clone the repository from GitHub. + * Install the required dependencies using the provided setup script. + * Follow the development environment setup guide in the repository's README file. + +3. **Contribution Guidelines:** + * Follow the coding standards and best practices outlined in the CONTRIBUTING.md file. + * Submit pull requests for code reviews before merging changes. + * Write unit tests for new features and bug fixes. + +4. **API Documentation:** + API documentation can be found in the docs/api directory of the repository. + +5. **Coding Standards:** + What coding standards or best practices should be followed? + * Follow the Kotlin coding conventions. + * Use meaningful variable and function names. + * Write clear and concise comments. + Are there any specific linting or formatting tools to be used? + * Use ktlint for linting and code formatting. + Are there any version control guidelines? + * Use Git for version control. + Are there any specific branch naming conventions or commit message guidelines? + * Use feature/branch-name for feature branches. + * Use fix/branch-name for bug fix branches. + * Follow the commit message guidelines outlined in the CONTRIBUTING.md file. + +## Architecture and Design + +1. **System Architecture:** + * Overview of the system architecture: + The Skyenet LLM Agent Platform is designed with a modular architecture to support real-time, interactive applications leveraging large language models. The core components include the LLM manager, session management, and dynamic content rendering. The system leverages LLMs for real-time interaction between the client and server, ensuring low latency and high + responsiveness. The architecture is divided into several layers: the presentation layer (UI components), the + application layer (session and task management), and the data layer (storage and retrieval of user data and + settings). + + * Diagrams (if any): + ![System Architecture Diagram](path/to/diagram.png) // Add a path to the actual diagram if available + +2. **Design Patterns:** + * What design patterns are used? + The Skyenet LLM Agent Platform employs several design patterns to ensure a robust and maintainable codebase: + + **Observer Pattern**: Used in the LLM manager to handle real-time updates and notifications. + **Factory Pattern**: Utilized for creating instances of various components like session tasks and UI elements. + **Singleton Pattern**: Ensures a single instance of core managers like `ApplicationServices` + - **Observer Pattern**: Used in the WebSocket manager to handle real-time updates and notifications. + - **Factory Pattern**: Utilized for creating instances of various components like session tasks and UI elements. + - **Singleton Pattern**: Ensures a single instance of core managers like `ApplicationServices` + and `SocketManagerBase`. + **Strategy Pattern**: Allows for flexible implementation of different retry mechanisms in the `Retryable` class. + **Decorator Pattern**: Used to extend the functionality of UI components dynamically. + - **Strategy Pattern**: Allows for flexible implementation of different retry mechanisms in the `Retryable` class. + - **Decorator Pattern**: Used to extend the functionality of UI components dynamically. + + * Why were they chosen? + These design patterns were chosen to promote code reusability, flexibility, and separation of concerns. The + Observer Pattern is ideal for real-time LLM interaction scenarios, while the Factory Pattern simplifies the creation + of complex objects. The Singleton Pattern ensures efficient resource management, and the Strategy Pattern allows + for easy customization of retry logic. The Decorator Pattern enables dynamic enhancement of UI components without + modifying their core functionality. + +3. **Technology Stack:** + * List of technologies and frameworks used: + + - **Frontend**: HTML, CSS, JavaScript, and Kotlin for the UI components and LLM interactions. + - **Backend**: Kotlin for server-side logic, leveraging Ktor for building asynchronous servers and managing LLM interactions. + - **LLMs**: Used for real-time interaction between the client and server. + - **Database**: HSQLDB for usage tracking, data storage, and managing LLM interactions. + **Cloud Services**: AWS S3 for file storage, AWS KMS for encryption. + **CI/CD**: GitHub Actions for continuous integration and deployment, Docker for containerization. + - **Cloud Services**: AWS S3 for file storage, AWS KMS for encryption. + - **CI/CD**: GitHub Actions for continuous integration and deployment, Docker for containerization. + + * Reasons for choosing these technologies: + The chosen technologies and frameworks provide a balance of performance, scalability, and developer productivity. Kotlin is used for both frontend and backend development to ensure consistency and leverage its modern language features. Ktor is selected for its lightweight and asynchronous capabilities, making it suitable for real-time LLM interactions. LLMs are essential for maintaining low-latency communication. HSQLDB is chosen for its + simplicity and ease of integration. AWS services are utilized for their reliability, security, and scalability. + GitHub Actions and Docker streamline the CI/CD process, ensuring efficient and reliable deployments. + +## Components and Modules + +1. **Component Overview:** + * **LLMManagerBase**: Manages LLM interactions, message queuing, and broadcasting. It handles the core LLM functionality, ensuring reliable communication between the client and server. + * **ApplicationInterface**: Provides methods to create interactive HTML elements and manage tasks. It acts as a + bridge between the task management logic and the LLM communication layer. + * **SessionTask**: Represents a task that can display progress and messages. It allows for dynamic updates to the UI + as tasks progress. + * **TabbedDisplay**: Manages a tabbed interface for displaying content. It supports adding, updating, and rendering + tabs dynamically. + * **Retryable**: Extends `TabbedDisplay` to add a retry mechanism for tasks. It allows for re-running a block of + code and capturing the new output in a new tab. + * **AgentPatterns**: Provides utility functions for displaying content in tabs. It helps in organizing and rendering + content efficiently. + * **Discussable**: Facilitates interactive discussions with users, allowing for feedback and revisions. It manages + user interactions and refines responses based on feedback. + * **UserSettingsManager**: Manages user settings, including loading and saving settings to disk. It ensures that + user preferences are maintained across sessions. + * **ClientManager**: Manages API clients, including creating and caching clients for sessions and users. It handles + the lifecycle of API clients and ensures efficient resource usage. + * **HSQLUsageManager**: Manages usage tracking using an HSQL database. It tracks resource usage and provides + summaries for monitoring and billing purposes. + * **AuthorizationManager**: Manages user authorization, checking if a user is authorized for specific operations. It + enforces access control policies. + * **AuthenticationManager**: Manages user authentication, including storing and retrieving users based on access + tokens. It ensures secure access to the system. + * **DataStorage**: Manages data storage, including storing and retrieving messages, sessions, and other data. It + provides a persistent storage layer for the application. + * **AwsPlatform**: Provides integration with AWS services, including S3 for file storage and KMS for encryption. It + leverages cloud services for scalability and security. + +2. **Inter-component Communication:** + * **LLM Communication**: `LLMManagerBase` handles the LLM interactions and message broadcasting. It + interacts with `ApplicationInterface` to send and receive messages. + * **Task Management**: `ApplicationInterface` creates and manages `SessionTask` instances. These tasks + use `TabbedDisplay` and `Retryable` to display content and handle retries. + * **User Interaction**: `Discussable` manages user interactions and collects feedback. It works with `SessionTask` + to update the UI based on user input. + * **Data Storage and Retrieval**: `DataStorage` handles the storage and retrieval of data. It interacts + with `UserSettingsManager`, `ClientManager`, and `HSQLUsageManager` to manage user settings, API clients, and + usage tracking. + * **Authorization and Authentication**: `AuthorizationManager` and `AuthenticationManager` enforce security + policies. They interact with other components to ensure secure access and operations. + * **Cloud Integration**: `AwsPlatform` provides cloud services for storage and encryption. It integrates + with `DataStorage` and other components to leverage AWS services. + +## Testing and Quality Assurance + +1. **Testing Strategy:** + What is the overall testing strategy? + * The testing strategy includes unit testing, integration testing, system testing, and acceptance testing. + What types of testing will be performed (unit, integration, system, acceptance)? + * **Unit Testing**: Testing individual components and functions. + * **Integration Testing**: Testing interactions between components. + * **System Testing**: Testing the entire system as a whole. + * **Acceptance Testing**: Testing the system against user requirements. + +2. **Test Cases:** + Test cases will be documented in the docs/test-cases directory of the repository. + +3. **CI/CD Pipeline:** + What is the CI/CD pipeline setup? + * The CI/CD pipeline includes automated testing, code quality checks, and deployment processes. + What tools and services are used for CI/CD? + * The pipeline uses GitHub Actions for automation, Docker for containerization, and AWS for deployment. + +4. **Code Reviews:** + What is the process for code reviews? + * Code reviews are conducted through pull requests on GitHub. + Who is responsible for code reviews? + * The development team is responsible for reviewing each other's code. \ No newline at end of file diff --git a/docs/project_info_template.md b/docs/project_info_template.md new file mode 100644 index 00000000..7c666486 --- /dev/null +++ b/docs/project_info_template.md @@ -0,0 +1,138 @@ +# Software Project Formalization and Documentation Questionnaire + +## General Information + +1. **Project Name:** + What is the name of the project? + +2. **Project Description:** + Provide a brief description of the project. + +3. **Project Objectives:** + What are the main objectives of the project? + +4. **Target Audience:** + Who are the intended users of the project? + +5. **Stakeholders:** + Who are the key stakeholders involved in the project? + +6. **Assumptions and Context:** + - What assumptions are being made about the project? + - What is the context in which this project is being developed? + - What problem is this project aiming to solve? + +## Scope and Deliverables + +1. **Scope:** + What is the scope of the project? + +2. **Deliverables:** + What are the key deliverables of the project? + +3. **Out of Scope:** + What is explicitly out of scope for the project? + +4. **Use Case Details:** + - What are the primary use cases for the project? + - Provide detailed descriptions of each use case. + +5. **Comparative Product Analysis:** + - Are there existing products that solve a similar problem? + - How does this project compare to those products? + +## Timeline + +1. **Start Date:** + When is the project starting? + +2. **End Date:** + When is the project expected to be completed? + +3. **Key Dates:** + Are there any key dates or deadlines that need to be met? + +4. **Milestones:** + List the major milestones and their expected completion dates. + +## Risks and Assumptions + +1. **Risks:** + Identify potential risks and their mitigation strategies. + +2. **Assumptions:** + - List any assumptions made during the planning of the project. + +## Project Closure + +1. **Closure Criteria:** + - What are the criteria for project closure? + - How will success be measured? + +2. **Post-Implementation Review:** + - Will there be a post-implementation review? + - What will be the focus of this review? + +3. **Lessons Learned:** + How will lessons learned be documented and shared? + +## Developer Documentation + +1. **Code Repository:** + Where will the code repository be hosted? + +2. **Development Setup:** + What are the steps to set up the development environment? + +3. **Contribution Guidelines:** + What are the guidelines for contributing to the project? + +4. **API Documentation:** + Where can developers find the API documentation? + +5. **Coding Standards:** + What coding standards or best practices should be followed? + Are there any specific linting or formatting tools to be used? + Are there any version control guidelines? + Are there any specific branch naming conventions or commit message guidelines? + +6. **Architectural Perspectives:** + - What are the different architectural perspectives of the project? + - Provide diagrams and descriptions for each perspective (e.g., logical, physical, deployment, etc.). + - What coding standards or best practices should be followed? + - Are there any specific linting or formatting tools to be used? + - Are there any version control guidelines? + - Are there any specific branch naming conventions or commit message guidelines? + +## Testing and Quality Assurance + +1. **Testing Strategy:** + - What is the overall testing strategy? + - What types of testing will be performed (unit, integration, system, acceptance)? + +2. **Test Cases:** + Where are the test cases documented? + +3. **CI/CD Pipeline:** + - What is the CI/CD pipeline setup? + - What tools and services are used for CI/CD? + +4. **Code Reviews:** + - What is the process for code reviews? + - Who is responsible for code reviews? + +## Maintenance and Support + +1. **Maintenance Plan:** + - What is the plan for ongoing maintenance? + - How will updates and patches be handled? + +2. **Support Plan:** + - What is the support plan post-deployment? + - Who will provide support and how can they be contacted? + +3. **Documentation:** + - What user documentation will be provided? + - Where can users find help and support documentation? + - What coding standards or best practices should be followed? + - List any assumptions made during the planning of the project. \ No newline at end of file diff --git a/docs/project_info_template2.md b/docs/project_info_template2.md new file mode 100644 index 00000000..334bc5f3 --- /dev/null +++ b/docs/project_info_template2.md @@ -0,0 +1,67 @@ +# Software Project Formalization and Documentation Questionnaire + +## General Information + +1. **Project Name:** +2. **Project Description:** + - What problem does this project solve? + - What are the key features and functionalities? +3. **Project Objectives:** + - What are the short-term and long-term goals? + - How will success be measured? +4. **Target Audience:** + - Who are the primary users? + - What are their needs and pain points? +5. **Scope:** + - What are the boundaries of the project? + - What will not be included in this project? + +## Developer Documentation + +1. **Code Repository:** + - Repository URL: + - Branching strategy: + - Contribution guidelines: +2. **Development Setup:** + - Prerequisites: + - Installation steps: + - Configuration details: + - How to run the project locally: + - How to run tests: + +## Architecture and Design + + 1. **System Architecture:** + - Overview of the system architecture: + - Diagrams (if any): + 2. **Design Patterns:** + - What design patterns are used? + - Why were they chosen? + 3. **Technology Stack:** + - List of technologies and frameworks used: + - Reasons for choosing these technologies: + +## Components and Modules + + 1. **Component Overview:** + - List of main components/modules: + - Brief description of each component/module: + 2. **Inter-component Communication:** + - How do components/modules interact with each other? + - Data flow between components/modules: + +## Testing and Quality Assurance + +1. **Testing Strategy:** + - Types of tests (unit, integration, end-to-end): + - Tools and frameworks used for testing: + - Test coverage goals: +2. **CI/CD Pipeline:** + - Overview of the CI/CD process: + - Tools and services used: + - Steps in the pipeline: +3. **Release Strategy:** + - Versioning scheme: + - Release frequency: + - Deployment process: + - Rollback plan: \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 1f10badb..190aaf72 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.77 +libraryVersion = 1.0.78 gradleVersion = 7.6.1 diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt index 44b1bfd9..ed697102 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt @@ -1,6 +1,7 @@ package com.simiacryptus.diff import org.apache.commons.text.similarity.LevenshteinDistance +import kotlin.math.floor object IterativePatchUtil { @@ -26,14 +27,14 @@ object IterativePatchUtil { } } - /** - * Normalizes a line by removing all whitespace. - * @param line The line to normalize. - * @return The normalized line. - */ - private fun normalizeLine(line: String): String { - return line.replace("\\s".toRegex(), "") - } + /** + * Normalizes a line by removing all whitespace. + * @param line The line to normalize. + * @return The normalized line. + */ + private fun normalizeLine(line: String): String { + return line.replace("\\s".toRegex(), "") + } /** * Applies a patch to the given source text. @@ -54,7 +55,7 @@ object IterativePatchUtil { // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links. // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found. -// linkByLevenshteinDistance(sourceLines, patchLines) + linkByLevenshteinDistance(sourceLines, patchLines) // Generate the patched text using the established links return generatePatchedTextUsingLinks(sourceLines, patchLines) @@ -83,10 +84,11 @@ object IterativePatchUtil { when { codeLine.matchingLine == null -> { // If the line is not matched and is adjacent to a non-matched line, add it as a context line - if(codeLine.nextLine?.matchingLine == null || codeLine.previousLine?.matchingLine == null) { + if (codeLine.nextLine?.matchingLine == null || codeLine.previousLine?.matchingLine == null) { patchedTextBuilder.appendLine(codeLine.line) } } + codeLine.matchingLine!!.type == LineType.DELETE -> null // Skip adding the line codeLine.matchingLine!!.type == LineType.CONTEXT -> patchedTextBuilder.appendLine(codeLine.line) codeLine.matchingLine!!.type == LineType.ADD -> throw IllegalStateException("ADD line is matched to source line") @@ -95,7 +97,7 @@ object IterativePatchUtil { // Add lines marked as ADD in the patch following the current matched line var nextPatchLine = codeLine.matchingLine?.nextLine while (nextPatchLine != null && nextPatchLine.matchingLine == null) { - when(nextPatchLine.type) { + when (nextPatchLine.type) { LineType.ADD -> patchedTextBuilder.appendLine(nextPatchLine.line) LineType.CONTEXT -> patchedTextBuilder.appendLine(nextPatchLine.line) LineType.DELETE -> null // Skip adding the line @@ -148,27 +150,27 @@ object IterativePatchUtil { while (patchPrev.type == LineType.ADD && patchPrev.previousLine != null) { patchPrev = patchPrev.previousLine!! } - if (normalizeLine(sourcePrev.line!!) == normalizeLine(patchPrev.line!!) && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { - if (sourcePrev.line.trim() == patchPrev.line!!.trim() && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { + if (sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { // Skip if there's already a match + if (normalizeLine(sourcePrev.line!!) == normalizeLine(patchPrev.line!!)) { // Check if the lines match exactly sourcePrev.matchingLine = patchPrev patchPrev.matchingLine = sourcePrev foundMatch = true } } + } - // Check the next line for a potential match - if (sourceLine.nextLine != null && patchLine.nextLine != null) { - val sourceNext = sourceLine.nextLine!! - var patchNext = patchLine.nextLine!! - while (patchNext.type == LineType.ADD && patchNext.nextLine != null) { - patchNext = patchNext.nextLine!! - } - if (normalizeLine(sourceNext.line!!) == normalizeLine(patchNext.line!!) && sourceNext.matchingLine == null && patchNext.matchingLine == null) { - if (sourceNext.line.trim() == patchNext.line!!.trim() && sourceNext.matchingLine == null && patchNext.matchingLine == null) { - sourceNext.matchingLine = patchNext - patchNext.matchingLine = sourceNext - foundMatch = true - } + // Check the next line for a potential match + if (sourceLine.nextLine != null && patchLine.nextLine != null) { + val sourceNext = sourceLine.nextLine!! + var patchNext = patchLine.nextLine!! + while (patchNext.type == LineType.ADD && patchNext.nextLine != null) { + patchNext = patchNext.nextLine!! + } + if (sourceNext.matchingLine == null && patchNext.matchingLine == null) { + if (normalizeLine(sourceNext.line!!) == normalizeLine(patchNext.line!!)) { + sourceNext.matchingLine = patchNext + patchNext.matchingLine = sourceNext + foundMatch = true } } } @@ -183,7 +185,7 @@ object IterativePatchUtil { */ private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) { val levenshteinDistance = LevenshteinDistance() - val maxDistance = (sourceLines.size + patchLines.size) / 10 // Increase max distance to allow more flexibility + val maxProximity = (sourceLines.size + patchLines.size) / 10 // Increase max distance to allow more flexibility // Iterate over source lines to find potential matches in the patch lines for (sourceLine in sourceLines) { @@ -200,18 +202,18 @@ object IterativePatchUtil { } }) { if (patchLine.matchingLine != null) continue // Skip lines that already have matches - + val maxDistance = minOf(bestDistance, floor(patchLine.line!!.length.toDouble() / 2).toInt()) // Calculate the Levenshtein distance between unmatched source and patch lines - val distance = levenshteinDistance.apply(normalizeLine(sourceLine.line!!), normalizeLine(patchLine.line!!)) + val distance = + levenshteinDistance.apply(normalizeLine(sourceLine.line!!), normalizeLine(patchLine.line!!)) if (distance <= maxDistance) { // Consider proximity to established links as a secondary factor val proximity = calculateProximityDistance(sourceLine, patchLine) + if(proximity > maxProximity) continue if (distance < bestDistance || (distance == bestDistance && proximity < bestProximity)) { - if (distance < bestDistance) { - bestMatch = patchLine - bestDistance = distance - bestProximity = proximity - } + bestMatch = patchLine + bestDistance = distance + bestProximity = proximity } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt index bd42be1d..b7ec96c6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Discussable.kt @@ -167,8 +167,6 @@ ${ history = history, task = newTask ).placeholder - }.apply { - set(label(size), process(container)) } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt index af81c1ac..58d3237e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt @@ -9,6 +9,14 @@ open class Retryable( val process: (StringBuilder) -> String ) : TabbedDisplay(task) { + init { + init() + } + + open fun init() { + set(label(size), process(container)) + } + override fun renderTabButtons(): String = """
${ tabs.withIndex().joinToString("\n") { (index, _) -> 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 15e2e7b0..c2b7f463 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 @@ -92,8 +92,6 @@ open class CodingAgent( } }, 100, TimeUnit.MILLISECONDS) newTask.placeholder - }.apply { - set(label(size), process(container)) } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt index 42896994..cb694b32 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt @@ -63,7 +63,7 @@ open class ChatSocketManager( onResponse(renderResponse, responseContents) renderResponse } - Retryable(ui, task, process).apply { set(label(size), process(container)) } + Retryable(ui, task, process) } catch (e: Exception) { log.info("Error in chat", e) task.error(ApplicationInterface(this), e) 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 567e2732..7800b20e 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 @@ -22,7 +22,7 @@ abstract class SessionTask( private val currentText: String get() = buffer.filter { it.isNotBlank() }.joinToString("") - private fun append( + fun append( htmlToAppend: String, showSpinner: Boolean ): StringBuilder? { diff --git a/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt b/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt index 2e5034ce..9bd1fe73 100644 --- a/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt +++ b/webui/src/test/kotlin/com/simiacryptus/diff/IterativePatchUtilTest.kt @@ -153,1432 +153,797 @@ class IterativePatchUtilTest { result.replace("\r\n", "\n").replace("\\s{1,}".toRegex(), " ") ) } + +/* + @Test fun testFromData2() { - val source = """ - // Importing the maze data from mazeData.js - import mazeData from './mazeData.js'; - - // Helper function to find an open position in the maze - function findOpenPosition(grid, startX, startY) { - for (let y = startY; y < grid.length; y++) { - for (let x = startX; x < grid[y].length; x++) { - if (grid[y][x] === 0) { - return { x: x, y: y }; - } - } - } - return { x: startX, y: startY }; // Fallback to the original position if no open position is found - } - // Variables to store maze size - let mazeWidth = 10; - let mazeHeight = 10; - // Select the first level as default - let { grid: maze, start, end } = mazeData.levels[0]; - let startPosition = { row: start.y, col: start.x }; - let endPosition = { row: end.y, col: end.x }; - // Variables to track the player's position and game status - let playerPosition = {...startPosition}; - let gameRunning = false; - let timerInterval; - let timeElapsed = 0; - - // Function to initialize the game - function initializeGame() { - document.getElementById('resetButton').addEventListener('click', restartGame); - document.getElementById('startButton').addEventListener('click', startGame); - document.getElementById('setSizeButton').addEventListener('click', setMazeSize); - document.addEventListener('keydown', handleKeyPress); - document.addEventListener('touchstart', handleTouchStart, { passive: false }); - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - drawMaze(); - placePlayer(); - } - - function setMazeSize() { - mazeWidth = parseInt(document.getElementById('mazeWidth').value); - mazeHeight = parseInt(document.getElementById('mazeHeight').value); - const newGrid = mazeData.generateMaze(mazeHeight, mazeWidth); - maze = newGrid; // Directly update the local 'maze' variable - // Ensure start and end are not placed inside walls - start = findOpenPosition(newGrid, 1, 1); - end = findOpenPosition(newGrid, mazeWidth - 2, mazeHeight - 2); - startPosition = { row: mazeData.levels[0].start.y, col: mazeData.levels[0].start.x }; - endPosition = { row: mazeData.levels[0].end.y, col: mazeData.levels[0].end.x }; - drawMaze(); - placePlayer(); - } - let touchStartX = 0; - let touchStartY = 0; - - function handleTouchStart(event) { - touchStartX = event.touches[0].clientX; - touchStartY = event.touches[0].clientY; - event.preventDefault(); - } - - function handleTouchMove(event) { - if (!touchStartX || !touchStartY) { - return; - } - - let touchMoveX = event.touches[0].clientX; - let touchMoveY = event.touches[0].clientY; - let diffX = touchStartX - touchMoveX; - let diffY = touchStartY - touchMoveY; - - if (Math.abs(diffX) > Math.abs(diffY)) { - // Horizontal movement - if (diffX > 0) { - // Left swipe - handleKeyPress({ key: 'ArrowLeft' }); - } else { - // Right swipe - handleKeyPress({ key: 'ArrowRight' }); - } - } else { - // Vertical movement - if (diffY > 0) { - // Up swipe - handleKeyPress({ key: 'ArrowUp' }); - } else { - // Down swipe - handleKeyPress({ key: 'ArrowDown' }); - } - } - - // Reset values - touchStartX = 0; - touchStartY = 0; - event.preventDefault(); - } - // Function to draw the maze - function drawMaze() { - const mazeContainer = document.getElementById('gameArea'); - mazeContainer.innerHTML = ''; // Clear previous maze - - maze.forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - const cellElement = document.createElement('div'); - cellElement.classList.add('cell', cell === 1 ? 'wall' : 'path'); - if (rowIndex === endPosition.row && colIndex === endPosition.col) { - cellElement.classList.add('end'); - } - cellElement.style.top = `${"$"}{rowIndex * 20}px`; - cellElement.style.left = `${"$"}{colIndex * 20}px`; - mazeContainer.appendChild(cellElement); - }); - }); - } - - // Function to place the player in the starting position - function placePlayer() { - const playerElement = document.createElement('div'); - playerElement.id = 'player'; - playerElement.style.top = `${"$"}{playerPosition.row * 20}px`; - playerElement.style.left = `${"$"}{playerPosition.col * 20}px`; - document.getElementById('gameArea').appendChild(playerElement); - } - - // Function to handle key press events - function handleKeyPress(event) { - if (!gameRunning) { - startGame(); - } - - let newPosition = {...playerPosition}; - switch (event.key) { - case 'ArrowUp': - newPosition.row--; - break; - case 'ArrowDown': - newPosition.row++; - break; - case 'ArrowLeft': - newPosition.col--; - break; - case 'ArrowRight': - newPosition.col++; - break; - default: - return; // Ignore other keys - } - - if (isValidMove(newPosition)) { - updatePlayerPosition(newPosition); - checkWinCondition(); - } - } - - // Function to check if the move is valid - function isValidMove(position) { - return maze[position.row] && maze[position.row][position.col] === 0; - } - - // Function to update the player's position - function updatePlayerPosition(position) { - playerPosition = position; - const playerElement = document.getElementById('player'); - playerElement.style.top = `${"$"}{position.row * 20}px`; - playerElement.style.left = `${"$"}{position.col * 20}px`; - } - - // Function to check win condition - function checkWinCondition() { - if (playerPosition.row === endPosition.row && playerPosition.col === endPosition.col) { - clearInterval(timerInterval); - gameRunning = false; - alert('Congratulations! You have completed the maze.'); - } - } - - // Function to start the game - function startGame() { - if (gameRunning) return; // Prevent restarting the game if it's already running - gameRunning = true; - timerInterval = setInterval(() => { - timeElapsed++; - document.getElementById('timer').textContent = formatTime(timeElapsed); - }, 1000); - } - - // Function to format time from seconds to MM:SS format - function formatTime(seconds) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${"$"}{padZero(minutes)}:${"$"}{padZero(remainingSeconds)}`; - } - - // Helper function to pad time values with zero - function padZero(number) { - return number.toString().padStart(2, '0'); - } - - // Function to restart the game - function restartGame() { - if (!gameRunning) startGame(); // Ensure game starts if not already running when reset - clearInterval(timerInterval); - timeElapsed = 0; - document.getElementById('timer').textContent = '00:00'; - playerPosition = {...startPosition}; - placePlayer(); - gameRunning = false; - } - - // Initialize the game when the window loads - window.onload = initializeGame; - """.trimIndent() - @Language("TEXT") val patch = """ - // Function to draw the maze - function drawMaze() { - const mazeContainer = document.getElementById('gameArea'); - mazeContainer.innerHTML = ''; // Clear previous maze - - + const cellWidth = mazeContainer.clientWidth / mazeWidth; - + const cellHeight = mazeContainer.clientHeight / mazeHeight; - - maze.forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - const cellElement = document.createElement('div'); - cellElement.classList.add('cell', cell === 1 ? 'wall' : 'path'); - if (rowIndex === endPosition.row && colIndex === endPosition.col) { - cellElement.classList.add('end'); - } - - cellElement.style.top = `${"$"}{rowIndex * 20}px`; - - cellElement.style.left = `${"$"}{colIndex * 20}px`; - - cellElement.style.width = '20px'; - - cellElement.style.height = '20px'; - + cellElement.style.top = `${"$"}{rowIndex * cellHeight}px`; - + cellElement.style.left = `${"$"}{colIndex * cellWidth}px`; - + cellElement.style.width = `${"$"}{cellWidth}px`; - + cellElement.style.height = `${"$"}{cellHeight}px`; - mazeContainer.appendChild(cellElement); - }); - }); - } - - // Function to place the player in the starting position - function placePlayer() { - const playerElement = document.createElement('div'); - playerElement.id = 'player'; - - playerElement.style.top = `${"$"}{playerPosition.row * 20}px`; - - playerElement.style.left = `${"$"}{playerPosition.col * 20}px`; - - playerElement.style.width = '20px'; - - playerElement.style.height = '20px'; - + const cellWidth = document.getElementById('gameArea').clientWidth / mazeWidth; - + const cellHeight = document.getElementById('gameArea').clientHeight / mazeHeight; - + playerElement.style.top = `${"$"}{playerPosition.row * cellHeight}px`; - + playerElement.style.left = `${"$"}{playerPosition.col * cellWidth}px`; - + playerElement.style.width = `${"$"}{cellWidth}px`; - + playerElement.style.height = `${"$"}{cellHeight}px`; - document.getElementById('gameArea').appendChild(playerElement); - } - - // Function to update the player's position - function updatePlayerPosition(position) { - playerPosition = position; - const playerElement = document.getElementById('player'); - - playerElement.style.top = `${"$"}{position.row * 20}px`; - - playerElement.style.left = `${"$"}{position.col * 20}px`; - + const cellWidth = document.getElementById('gameArea').clientWidth / mazeWidth; - + const cellHeight = document.getElementById('gameArea').clientHeight / mazeHeight; - + playerElement.style.top = `${"$"}{position.row * cellHeight}px`; - + playerElement.style.left = `${"$"}{position.col * cellWidth}px`; - } - """.trimIndent() - val expected = """ - // Importing the maze data from mazeData.js - import mazeData from './mazeData.js'; - - // Helper function to find an open position in the maze - function findOpenPosition(grid, startX, startY) { - for (let y = startY; y < grid.length; y++) { - for (let x = startX; x < grid[y].length; x++) { - if (grid[y][x] === 0) { - return { x: x, y: y }; - } - } - } - return { x: startX, y: startY }; // Fallback to the original position if no open position is found - } - // Variables to store maze size - let mazeWidth = 10; - let mazeHeight = 10; - // Select the first level as default - let { grid: maze, start, end } = mazeData.levels[0]; - let startPosition = { row: start.y, col: start.x }; - let endPosition = { row: end.y, col: end.x }; - // Variables to track the player's position and game status - let playerPosition = {...startPosition}; - let gameRunning = false; - let timerInterval; - let timeElapsed = 0; - - // Function to initialize the game - function initializeGame() { - document.getElementById('resetButton').addEventListener('click', restartGame); - document.getElementById('startButton').addEventListener('click', startGame); - document.getElementById('setSizeButton').addEventListener('click', setMazeSize); - document.addEventListener('keydown', handleKeyPress); - document.addEventListener('touchstart', handleTouchStart, { passive: false }); - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - drawMaze(); - placePlayer(); - } - - function setMazeSize() { - mazeWidth = parseInt(document.getElementById('mazeWidth').value); - mazeHeight = parseInt(document.getElementById('mazeHeight').value); - const newGrid = mazeData.generateMaze(mazeHeight, mazeWidth); - maze = newGrid; // Directly update the local 'maze' variable - // Ensure start and end are not placed inside walls - start = findOpenPosition(newGrid, 1, 1); - end = findOpenPosition(newGrid, mazeWidth - 2, mazeHeight - 2); - startPosition = { row: mazeData.levels[0].start.y, col: mazeData.levels[0].start.x }; - endPosition = { row: mazeData.levels[0].end.y, col: mazeData.levels[0].end.x }; - drawMaze(); - placePlayer(); - } - let touchStartX = 0; - let touchStartY = 0; - - function handleTouchStart(event) { - touchStartX = event.touches[0].clientX; - touchStartY = event.touches[0].clientY; - event.preventDefault(); - } - - function handleTouchMove(event) { - if (!touchStartX || !touchStartY) { - return; - } - - let touchMoveX = event.touches[0].clientX; - let touchMoveY = event.touches[0].clientY; - let diffX = touchStartX - touchMoveX; - let diffY = touchStartY - touchMoveY; - - if (Math.abs(diffX) > Math.abs(diffY)) { - // Horizontal movement - if (diffX > 0) { - // Left swipe - handleKeyPress({ key: 'ArrowLeft' }); - } else { - // Right swipe - handleKeyPress({ key: 'ArrowRight' }); - } - } else { - // Vertical movement - if (diffY > 0) { - // Up swipe - handleKeyPress({ key: 'ArrowUp' }); - } else { - // Down swipe - handleKeyPress({ key: 'ArrowDown' }); - } - } - - // Reset values - touchStartX = 0; - touchStartY = 0; - event.preventDefault(); - } - // Function to draw the maze - function drawMaze() { - const mazeContainer = document.getElementById('gameArea'); - mazeContainer.innerHTML = ''; // Clear previous maze - - const cellWidth = mazeContainer.clientWidth / mazeWidth; - const cellHeight = mazeContainer.clientHeight / mazeHeight; - - maze.forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - const cellElement = document.createElement('div'); - cellElement.classList.add('cell', cell === 1 ? 'wall' : 'path'); - if (rowIndex === endPosition.row && colIndex === endPosition.col) { - cellElement.classList.add('end'); - } - cellElement.style.top = `${"$"}{rowIndex * cellHeight}px`; - cellElement.style.left = `${"$"}{colIndex * cellWidth}px`; - cellElement.style.width = `${"$"}{cellWidth}px`; - cellElement.style.height = `${"$"}{cellHeight}px`; - mazeContainer.appendChild(cellElement); - }); - }); - } - - // Function to place the player in the starting position - function placePlayer() { - const playerElement = document.createElement('div'); - playerElement.id = 'player'; - const cellWidth = document.getElementById('gameArea').clientWidth / mazeWidth; - const cellHeight = document.getElementById('gameArea').clientHeight / mazeHeight; - playerElement.style.top = `${"$"}{playerPosition.row * cellHeight}px`; - playerElement.style.left = `${"$"}{playerPosition.col * cellWidth}px`; - playerElement.style.width = `${"$"}{cellWidth}px`; - playerElement.style.height = `${"$"}{cellHeight}px`; - document.getElementById('gameArea').appendChild(playerElement); - } - - // Function to handle key press events - function handleKeyPress(event) { - if (!gameRunning) { - startGame(); - } - - let newPosition = {...playerPosition}; - switch (event.key) { - case 'ArrowUp': - newPosition.row--; - break; - case 'ArrowDown': - newPosition.row++; - break; - case 'ArrowLeft': - newPosition.col--; - break; - case 'ArrowRight': - newPosition.col++; - break; - default: - return; // Ignore other keys - } - - if (isValidMove(newPosition)) { - updatePlayerPosition(newPosition); - checkWinCondition(); - } - } - - // Function to check if the move is valid - function isValidMove(position) { - return maze[position.row] && maze[position.row][position.col] === 0; - } - - // Function to update the player's position - function updatePlayerPosition(position) { - playerPosition = position; - const playerElement = document.getElementById('player'); - const cellWidth = document.getElementById('gameArea').clientWidth / mazeWidth; - const cellHeight = document.getElementById('gameArea').clientHeight / mazeHeight; - playerElement.style.top = `${"$"}{position.row * cellHeight}px`; - playerElement.style.left = `${"$"}{position.col * cellWidth}px`; - } - - // Function to check win condition - function checkWinCondition() { - if (playerPosition.row === endPosition.row && playerPosition.col === endPosition.col) { - clearInterval(timerInterval); - gameRunning = false; - alert('Congratulations! You have completed the maze.'); - } - } - - // Function to start the game - function startGame() { - if (gameRunning) return; // Prevent restarting the game if it's already running - gameRunning = true; - timerInterval = setInterval(() => { - timeElapsed++; - document.getElementById('timer').textContent = formatTime(timeElapsed); - }, 1000); - } - - // Function to format time from seconds to MM:SS format - function formatTime(seconds) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${"$"}{padZero(minutes)}:${"$"}{padZero(remainingSeconds)}`; - } - - // Helper function to pad time values with zero - function padZero(number) { - return number.toString().padStart(2, '0'); - } - - // Function to restart the game - function restartGame() { - if (!gameRunning) startGame(); // Ensure game starts if not already running when reset - clearInterval(timerInterval); - timeElapsed = 0; - document.getElementById('timer').textContent = '00:00'; - playerPosition = {...startPosition}; - placePlayer(); - gameRunning = false; - } - - // Initialize the game when the window loads - window.onload = initializeGame; - """.trimIndent() - val result = IterativePatchUtil.patch(source, patch) - Assertions.assertEquals( - expected.replace("\r\n", "\n").replace("[ \\t]{1,}".toRegex(), " "), - result.replace("\r\n", "\n").replace("[ \\t]{1,}".toRegex(), " ") - ) + @Language("KT") val source = """ +type RGB = { + r: number; + g: number; + b: number; +}; + +export class ColorMixer { + // ... other methods ... + + public static hexToRgb(hex: string): RGB { + // ... existing implementation ... } - @Test - fun testFromData3() { - val source = """ -package com.simiacryptus.diff -import org.apache.commons.text.similarity.LevenshteinDistance - -object IterativePatchUtil { - - enum class LineType { CONTEXT, ADD, DELETE } - class LineRecord( - val index: Int, - val line: String, - var previousLine: LineRecord? = null, - var nextLine: LineRecord? = null, - var matchingLine: LineRecord? = null, - var type: LineType = LineType.CONTEXT - ) { - override fun toString(): String { - val sb = StringBuilder() - when (type) { - LineType.CONTEXT -> sb.append(" ") - LineType.ADD -> sb.append("+") - LineType.DELETE -> sb.append("-") - } - sb.append(" ") - sb.append(line) - return sb.toString() - } + public static convertHexToRgb(hex: string): RGB { + return this.hexToRgb(hex); } - /** - * Applies a patch to the given source text. - * @param source The original text. - * @param patch The patch to apply. - * @return The text after the patch has been applied. - */ - fun patch(source: String, patch: String): String { - // Parse the source and patch texts into lists of line records - val sourceLines = parseLines(source) - val patchLines = parsePatchLines(patch) - - // Step 1: Link all unique lines in the source and patch that match exactly - linkUniqueMatchingLines(sourceLines, patchLines) - - // Step 2: Link all exact matches in the source and patch which are adjacent to established links - linkAdjacentMatchingLines(sourceLines) - - // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links. - // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found. -// linkByLevenshteinDistance(sourceLines, patchLines) - - // Generate the patched text using the established links - return generatePatchedTextUsingLinks(sourceLines, patchLines) + private static hexToRgb(hex: string): RGB { + // ... other methods ... + public + mixColors(...colors + : + string[] + ): + string + { + // Implementation of color mixing logic + // This is a placeholder implementation + return colors[0]; // Return the first color for now + } + + // ... existing methods ... } - /** - * Generates the final patched text using the links established between the source and patch lines. - * @param sourceLines The source lines with established links. - * @param patchLines The patch lines with established links. - * @return The final patched text. - */ - private fun generatePatchedTextUsingLinks(sourceLines: List, patchLines: List): String { - val patchedTextBuilder = StringBuilder() - val sourceLineBuffer = sourceLines.toMutableList() - - // Add any leading 'add' lines from the patch - val patchLines = patchLines.toMutableList() - while (patchLines.firstOrNull()?.type == LineType.ADD) { - patchedTextBuilder.appendLine(patchLines.removeFirst().line) - } + private static clamp(value: number): number { + return Math.max(0, Math.min(255, Math.round(value))); + } - // Process the rest of the lines - while (sourceLineBuffer.isNotEmpty()) { - // Copy all lines until the next matched line - val codeLine = sourceLineBuffer.removeFirst() - when { - codeLine.matchingLine == null -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.DELETE -> null // Skip adding the line - codeLine.matchingLine!!.type == LineType.CONTEXT -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.ADD -> throw IllegalStateException("ADD line is matched to source line") - } + private static rgbToHex(rgb: RGB): string { + return `#${'$'}{ColorMixer.clamp(rgb.r).toString(16).padStart(2, '0')}${'$'}{ColorMixer.clamp(rgb.g).toString(16).padStart(2, '0')}${'$'}{ColorMixer.clamp(rgb.b).toString(16).padStart(2, '0')}`; + } - // Add lines marked as ADD in the patch following the current matched line - var nextPatchLine = codeLine.matchingLine?.nextLine - while (nextPatchLine != null && nextPatchLine.matchingLine == null) { - when(nextPatchLine.type) { - LineType.ADD -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.CONTEXT -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.DELETE -> null // Skip adding the line - } - nextPatchLine = nextPatchLine.nextLine - } - } - return patchedTextBuilder.toString().trimEnd() + // ... other methods ... +} + +// ... other methods ... +} +const result = "/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})${'$'}/i".exec(hex); +return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) +} : {r: 0, g: 0, b: 0}; +} + +static +mixAdditive(...colors +: +string[] +): +string +{ + if (colors.length === 0) return '#000000'; + + const mixed: RGB = colors.reduce((acc, color) => { + const rgb = ColorMixer.hexToRgb(color); + return { + r: acc.r + rgb.r, + g: acc.g + rgb.g, + b: acc.b + rgb.b + }; + }, {r: 0, g: 0, b: 0}); + + const count = colors.length; + mixed.r = ColorMixer.clamp(mixed.r / count); + mixed.g = ColorMixer.clamp(mixed.g / count); + mixed.b = ColorMixer.clamp(mixed.b / count); + + return ColorMixer.rgbToHex(mixed); +} + +static +mixSubtractive(...colors +: +string[] +): +string +{ + if (colors.length === 0) return '#FFFFFF'; + + const mixed: RGB = colors.reduce((acc, color) => { + const rgb = ColorMixer.hexToRgb(color); + return { + r: acc.r * (rgb.r / 255), + g: acc.g * (rgb.g / 255), + b: acc.b * (rgb.b / 255) + }; + }, {r: 255, g: 255, b: 255}); + + mixed.r = ColorMixer.clamp(mixed.r); + mixed.g = ColorMixer.clamp(mixed.g); + mixed.b = ColorMixer.clamp(mixed.b); + + return ColorMixer.rgbToHex(mixed); +} + +static +getComplementaryColor(color +: +string +): +string +{ + const rgb = ColorMixer.hexToRgb(color); + const complementary: RGB = { + r: 255 - rgb.r, + g: 255 - rgb.g, + b: 255 - rgb.b + }; + return ColorMixer.rgbToHex(complementary); +} + +static +getLightness(color +: +string +): +number +{ + const rgb = ColorMixer.hexToRgb(color); + return (Math.max(rgb.r, rgb.g, rgb.b) + Math.min(rgb.r, rgb.g, rgb.b)) / (2 * 255); +} + +static +adjustLightness(color +: +string, amount +: +number +): +string +{ + const rgb = ColorMixer.hexToRgb(color); + const hsl = ColorMixer.rgbToHsl(rgb); + hsl.l = Math.max(0, Math.min(1, hsl.l + amount)); + return ColorMixer.rgbToHex(ColorMixer.hslToRgb(hsl)); +} + +private static +rgbToHsl(rgb +: +RGB +): +{ + number, s +: + number, l +: + number +} +{ + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h: number; + const l = (max + min) / 2; + + if (max === min) { + h = 0; // Achromatic + } else { + h = s = 0; + } +else + { + const d = max - min; + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + default: + h = 0; // This should never happen, but it satisfies the type checker + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h = h / 6; } - /** - * Links lines between the source and the patch that are unique and match exactly. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) { - val sourceLineMap = sourceLines.groupBy { it.line.trim() } - val patchLineMap = patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }.groupBy { it.line.trim() } - - sourceLineMap.keys.intersect(patchLineMap.keys).forEach { key -> - val sourceLine = sourceLineMap[key]?.singleOrNull() - val patchLine = patchLineMap[key]?.singleOrNull() - if (sourceLine != null && patchLine != null) { - sourceLine.matchingLine = patchLine - patchLine.matchingLine = sourceLine - } - } + return {h, s, l}; +} + +private static +hslToRgb(hsl +: +{ + number, s +: + number, l +: + number +} +): +RGB +{ + const {h, s, l} = hsl; + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); } - /** - * Links lines that are adjacent to already linked lines and match exactly. - * @param sourceLines The source lines with some established links. - */ - private fun linkAdjacentMatchingLines(sourceLines: List) { - var foundMatch = true - while (foundMatch) { - foundMatch = false - for (sourceLine in sourceLines) { - val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line - - // Check the previous line for a potential match - if (sourceLine.previousLine != null && patchLine.previousLine != null) { - val sourcePrev = sourceLine.previousLine!! - var patchPrev = patchLine.previousLine!! - while (patchPrev.type == LineType.ADD && patchPrev.previousLine != null) { - patchPrev = patchPrev.previousLine!! - } - if (sourcePrev.line.trim() == patchPrev.line.trim() && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { - sourcePrev.matchingLine = patchPrev - patchPrev.matchingLine = sourcePrev - foundMatch = true - } - } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} +} + """.trimIndent() + val patch = """ + | export class ColorMixer { + |+ public mixColors(...colors: string[]): string { + |+ if (colors.length === 0) return '#000000'; + |+ if (colors.length === 1) return colors[0]; + |+ + |+ const rgbColors = colors.map(color => this.hexToRgb(color)); + |+ const mixedRgb = rgbColors.reduce((acc, rgb) => ({ + |+ r: acc.r + rgb.r, + |+ g: acc.g + rgb.g, + |+ b: acc.b + rgb.b + |+ })); + |+ + |+ const avgRgb = { + |+ r: Math.round(mixedRgb.r / colors.length), + |+ g: Math.round(mixedRgb.g / colors.length), + |+ b: Math.round(mixedRgb.b / colors.length) + |+ }; + |+ + |+ return this.rgbToHex(avgRgb); + |+ } + | + | public static hexToRgb(hex: string): RGB { + | // Existing implementation... + | } + | + | private static rgbToHex(rgb: RGB): string { + | // Existing implementation... + | } + | + | // Other existing methods... + | } + """.trimMargin() + val expected = """ +type RGB = { + r: number; + g: number; + b: number; +}; + +export class ColorMixer { + public mixColors(...colors: string[]): string { + if (colors.length === 0) return '#000000'; + if (colors.length === 1) return colors[0]; + + const rgbColors = colors.map(color => this.hexToRgb(color)); + const mixedRgb = rgbColors.reduce((acc, rgb) => ({ + r: acc.r + rgb.r, + g: acc.g + rgb.g, + b: acc.b + rgb.b + })); + const avgRgb = { + r: Math.round(mixedRgb.r / colors.length), + g: Math.round(mixedRgb.g / colors.length), + b: Math.round(mixedRgb.b / colors.length) + }; + return this.rgbToHex(avgRgb); + } + // ... other methods ... - // Check the next line for a potential match - if (sourceLine.nextLine != null && patchLine.nextLine != null) { - val sourceNext = sourceLine.nextLine!! - var patchNext = patchLine.nextLine!! - while (patchNext.type == LineType.ADD && patchNext.nextLine != null) { - patchNext = patchNext.nextLine!! - } - if (sourceNext.line.trim() == patchNext.line.trim() && sourceNext.matchingLine == null && patchNext.matchingLine == null) { - sourceNext.matchingLine = patchNext - patchNext.matchingLine = sourceNext - foundMatch = true - } - } - } - } + public static hexToRgb(hex: string): RGB { + // ... existing implementation ... } - /** - * Establishes links between source and patch lines based on the Levenshtein distance and proximity to already established links. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) { - val levenshteinDistance = LevenshteinDistance() - val maxDistance = (sourceLines.size + patchLines.size) / 10 // Increase max distance to allow more flexibility - - // Iterate over source lines to find potential matches in the patch lines - for (sourceLine in sourceLines) { - if (sourceLine.matchingLine != null) continue // Skip lines that already have matches - - var bestMatch: LineRecord? = null - var bestDistance = Int.MAX_VALUE - var bestProximity = Int.MAX_VALUE - - for (patchLine in patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }) { - if (patchLine.matchingLine != null) continue // Skip lines that already have matches - - // Calculate the Levenshtein distance between unmatched source and patch lines - val distance = levenshteinDistance.apply(sourceLine.line.trim(), patchLine.line.trim()) - if (distance <= maxDistance) { - // Consider proximity to established links as a secondary factor - val proximity = calculateProximityDistance(sourceLine, patchLine) - if (distance < bestDistance || (distance == bestDistance && proximity < bestProximity)) { - if (distance < bestDistance) { - bestMatch = patchLine - bestDistance = distance - bestProximity = proximity - } - } - } + public static convertHexToRgb(hex: string): RGB { + return this.hexToRgb(hex); + } - // Establish the best match found, if any - if (bestMatch != null) { - sourceLine.matchingLine = bestMatch - bestMatch.matchingLine = sourceLine - } - } - } + private static hexToRgb(hex: string): RGB { + // ... other methods ... + public + mixColors(...colors + : + string[] + ): + string + { + // Implementation of color mixing logic + // This is a placeholder implementation + return colors[0]; // Return the first color for now + } + + // ... existing methods ... } - /** - * Calculates the proximity distance between a source line and a patch line based on their distance to the nearest established link. - * @param sourceLine The source line. - * @param patchLine The patch line. - * @return The proximity distance. - */ - private fun calculateProximityDistance(sourceLine: LineRecord, patchLine: LineRecord): Int { - // Find the nearest established link in both directions for source and patch lines - var sourceDistancePrev = 0 - var sourceDistanceNext = 0 - var patchDistancePrev = 0 - var patchDistanceNext = 0 - - var currentSourceLine = sourceLine.previousLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistancePrev++ - currentSourceLine = currentSourceLine.previousLine - } + private static clamp(value: number): number { + return Math.max(0, Math.min(255, Math.round(value))); + } - currentSourceLine = sourceLine.nextLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistanceNext++ - currentSourceLine = currentSourceLine.nextLine - } + private static rgbToHex(rgb: RGB): string { + return `#${'$'}{ColorMixer.clamp(rgb.r).toString(16).padStart(2, '0')}${'$'}{ColorMixer.clamp(rgb.g).toString(16).padStart(2, '0')}${'$'}{ColorMixer.clamp(rgb.b).toString(16).padStart(2, '0')}`; + } - var currentPatchLine = patchLine.previousLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistancePrev++ - currentPatchLine = currentPatchLine.previousLine - } + // ... other methods ... +} - currentPatchLine = patchLine.nextLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistanceNext++ - currentPatchLine = currentPatchLine.nextLine - } +// ... other methods ... +} +const result = "/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})${'$'}/i".exec(hex); +return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) +} : {r: 0, g: 0, b: 0}; +} - // Calculate the total proximity distance as the sum of minimum distances in each direction - return minOf(sourceDistancePrev, patchDistancePrev) + minOf(sourceDistanceNext, patchDistanceNext) - } +static +mixAdditive(...colors +: +string[] +): +string +{ + if (colors.length === 0) return '#000000'; + + const mixed: RGB = colors.reduce((acc, color) => { + const rgb = ColorMixer.hexToRgb(color); + return { + r: acc.r + rgb.r, + g: acc.g + rgb.g, + b: acc.b + rgb.b + }; + }, {r: 0, g: 0, b: 0}); + + const count = colors.length; + mixed.r = ColorMixer.clamp(mixed.r / count); + mixed.g = ColorMixer.clamp(mixed.g / count); + mixed.b = ColorMixer.clamp(mixed.b / count); + + return ColorMixer.rgbToHex(mixed); +} - /** - * Parses the given text into a list of line records. - * @param text The text to parse. - * @return The list of line records. - */ - private fun parseLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord(index, line) - }) - - /** - * Sets the previous and next line links for a list of line records. - * @param list The list of line records. - * @return The list with links set. - */ - private fun setLinks(list: List): List { - for (i in 0 until list.size) { - list[i].previousLine = if (i > 0) list[i - 1] else null - list[i].nextLine = if (i < list.size - 1) list[i + 1] else null - } - return list +static +mixSubtractive(...colors +: +string[] +): +string +{ + if (colors.length === 0) return '#FFFFFF'; + + const mixed: RGB = colors.reduce((acc, color) => { + const rgb = ColorMixer.hexToRgb(color); + return { + r: acc.r * (rgb.r / 255), + g: acc.g * (rgb.g / 255), + b: acc.b * (rgb.b / 255) + }; + }, {r: 255, g: 255, b: 255}); + + mixed.r = ColorMixer.clamp(mixed.r); + mixed.g = ColorMixer.clamp(mixed.g); + mixed.b = ColorMixer.clamp(mixed.b); + + return ColorMixer.rgbToHex(mixed); +} + +static +getComplementaryColor(color +: +string +): +string +{ + const rgb = ColorMixer.hexToRgb(color); + const complementary: RGB = { + r: 255 - rgb.r, + g: 255 - rgb.g, + b: 255 - rgb.b + }; + return ColorMixer.rgbToHex(complementary); +} + +static +getLightness(color +: +string +): +number +{ + const rgb = ColorMixer.hexToRgb(color); + return (Math.max(rgb.r, rgb.g, rgb.b) + Math.min(rgb.r, rgb.g, rgb.b)) / (2 * 255); +} + +static +adjustLightness(color +: +string, amount +: +number +): +string +{ + const rgb = ColorMixer.hexToRgb(color); + const hsl = ColorMixer.rgbToHsl(rgb); + hsl.l = Math.max(0, Math.min(1, hsl.l + amount)); + return ColorMixer.rgbToHex(ColorMixer.hslToRgb(hsl)); +} + +private static +rgbToHsl(rgb +: +RGB +): +{ + number, s +: + number, l +: + number +} +{ + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h: number; + const l = (max + min) / 2; + + if (max === min) { + h = 0; // Achromatic + } else { + h = s = 0; + } +else + { + const d = max - min; + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + default: + h = 0; // This should never happen, but it satisfies the type checker + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h = h / 6; } - /** - * Parses the patch text into a list of line records, identifying the type of each line (ADD, DELETE, CONTEXT). - * @param text The patch text to parse. - * @return The list of line records with types set. - */ - private fun parsePatchLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord( - index = index, line = line.let { - when { - it.trimStart().startsWith("+") -> it.trimStart().substring(1) - it.trimStart().startsWith("-") -> it.trimStart().substring(1) - else -> it - } - }, type = when { - line.startsWith("+") -> LineType.ADD - line.startsWith("-") -> LineType.DELETE - else -> LineType.CONTEXT - } - ) - }) + return {h, s, l}; +} +private static +hslToRgb(hsl +: +{ + number, s +: + number, l +: + number } +): +RGB +{ + const {h, s, l} = hsl; + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} +} """.trimIndent() - @Language("TEXT") val patch = """ -package com.simiacryptus.diff + val result = IterativePatchUtil.patch(source, patch) + Assertions.assertEquals( + expected.replace("\r\n", "\n"),//.replace("\\s{2,}".toRegex(), " "), + result.replace("\r\n", "\n")//.replace("\\s{2,}".toRegex(), " ") + ) + } -import org.apache.commons.text.similarity.LevenshteinDistance - -object IterativePatchUtil { - - enum class LineType { CONTEXT, ADD, DELETE } - class LineRecord( - val index: Int, - val line: String, - var previousLine: LineRecord? = null, - var nextLine: LineRecord? = null, - var matchingLine: LineRecord? = null, - var type: LineType = LineType.CONTEXT - ) { - override fun toString(): String { - val sb = StringBuilder() - when (type) { - LineType.CONTEXT -> sb.append(" ") - LineType.ADD -> sb.append("+") - LineType.DELETE -> sb.append("-") - } - sb.append(" ") - sb.append(line) - return sb.toString() - } + @Test + fun testFromData3() { + val source = """ +import {Scene} from './Scene'; + +export class Game { + private currentScene: Scene | null = null; + private isRunning: boolean = false; + private lastTimestamp: number = 0; + + constructor(private canvas: HTMLCanvasElement) { } -+ /** -+ * Normalizes a line by removing all whitespace. -+ * @param line The line to normalize. -+ * @return The normalized line. -+ */ -+ private fun normalizeLine(line: String): String { -+ return line.replace("\\s".toRegex(), "") -+ } - - /** - * Applies a patch to the given source text. - * @param source The original text. - * @param patch The patch to apply. - * @return The text after the patch has been applied. - */ - fun patch(source: String, patch: String): String { - // Parse the source and patch texts into lists of line records - val sourceLines = parseLines(source) - val patchLines = parsePatchLines(patch) - - // Step 1: Link all unique lines in the source and patch that match exactly - linkUniqueMatchingLines(sourceLines, patchLines) - - // Step 2: Link all exact matches in the source and patch which are adjacent to established links - linkAdjacentMatchingLines(sourceLines) - - // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links. - // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found. -// linkByLevenshteinDistance(sourceLines, patchLines) - - // Generate the patched text using the established links - return generatePatchedTextUsingLinks(sourceLines, patchLines) + public start(): void { + this.isRunning = true; + this.lastTimestamp = performance.now(); + requestAnimationFrame(this.gameLoop.bind(this)); } - /** - * Generates the final patched text using the links established between the source and patch lines. - * @param sourceLines The source lines with established links. - * @param patchLines The patch lines with established links. - * @return The final patched text. - */ - private fun generatePatchedTextUsingLinks(sourceLines: List, patchLines: List): String { - val patchedTextBuilder = StringBuilder() - val sourceLineBuffer = sourceLines.toMutableList() - - // Add any leading 'add' lines from the patch - val patchLines = patchLines.toMutableList() - while (patchLines.firstOrNull()?.type == LineType.ADD) { - patchedTextBuilder.appendLine(patchLines.removeFirst().line) - } + public stop(): void { + this.isRunning = false; + } - // Process the rest of the lines - while (sourceLineBuffer.isNotEmpty()) { - // Copy all lines until the next matched line - val codeLine = sourceLineBuffer.removeFirst() - when { - codeLine.matchingLine == null -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.DELETE -> null // Skip adding the line - codeLine.matchingLine!!.type == LineType.CONTEXT -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.ADD -> throw IllegalStateException("ADD line is matched to source line") - } + public setScene(scene: Scene): void { + this.currentScene = scene; + } - // Add lines marked as ADD in the patch following the current matched line - var nextPatchLine = codeLine.matchingLine?.nextLine - while (nextPatchLine != null && nextPatchLine.matchingLine == null) { - when(nextPatchLine.type) { - LineType.ADD -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.CONTEXT -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.DELETE -> null // Skip adding the line - } - nextPatchLine = nextPatchLine.nextLine - } - } - return patchedTextBuilder.toString().trimEnd() + private gameLoop(timestamp: number): void { + if (!this.isRunning) return; + + const deltaTime = (timestamp - this.lastTimestamp) / 1000; // Convert to seconds + this.lastTimestamp = timestamp; + + this.update(deltaTime); + this.render(); + + requestAnimationFrame(this.gameLoop.bind(this)); } - /** - * Links lines between the source and the patch that are unique and match exactly. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) { - val sourceLineMap = sourceLines.groupBy { normalizeLine(it.line) } - val patchLineMap = patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }.groupBy { normalizeLine(it.line) } - - sourceLineMap.keys.intersect(patchLineMap.keys).forEach { key -> - val sourceLine = sourceLineMap[key]?.singleOrNull() - val patchLine = patchLineMap[key]?.singleOrNull() - if (sourceLine != null && patchLine != null) { - sourceLine.matchingLine = patchLine - patchLine.matchingLine = sourceLine - } + private update(deltaTime: number): void { + if (this.currentScene) { + this.currentScene.update(deltaTime); } } - /** - * Links lines that are adjacent to already linked lines and match exactly. - * @param sourceLines The source lines with some established links. - */ - private fun linkAdjacentMatchingLines(sourceLines: List) { - var foundMatch = true - while (foundMatch) { - foundMatch = false - for (sourceLine in sourceLines) { - val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line - - // Check the previous line for a potential match - if (sourceLine.previousLine != null && patchLine.previousLine != null) { - val sourcePrev = sourceLine.previousLine!! - var patchPrev = patchLine.previousLine!! - while (patchPrev.type == LineType.ADD && patchPrev.previousLine != null) { - patchPrev = patchPrev.previousLine!! - } - if (normalizeLine(sourcePrev.line) == normalizeLine(patchPrev.line) && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { - sourcePrev.matchingLine = patchPrev - patchPrev.matchingLine = sourcePrev - foundMatch = true - } - } + private render(): void { + const ctx = this.canvas.getContext('2d'); + if (!ctx) return; - // Check the next line for a potential match - if (sourceLine.nextLine != null && patchLine.nextLine != null) { - val sourceNext = sourceLine.nextLine!! - var patchNext = patchLine.nextLine!! - while (patchNext.type == LineType.ADD && patchNext.nextLine != null) { - patchNext = patchNext.nextLine!! - } - if (normalizeLine(sourceNext.line) == normalizeLine(patchNext.line) && sourceNext.matchingLine == null && patchNext.matchingLine == null) { - sourceNext.matchingLine = patchNext - patchNext.matchingLine = sourceNext - foundMatch = true - } - } - } + // Clear the canvas + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + if (this.currentScene) { + this.currentScene.render(ctx); } } +} + """.trimIndent() + val patch = """ + constructor(private canvas: HTMLCanvasElement) { ++ this.width = canvas.width; ++ this.height = canvas.height; + } - /** - * Establishes links between source and patch lines based on the Levenshtein distance and proximity to already established links. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) { - val levenshteinDistance = LevenshteinDistance() - val maxDistance = (sourceLines.size + patchLines.size) / 10 // Increase max distance to allow more flexibility - - // Iterate over source lines to find potential matches in the patch lines - for (sourceLine in sourceLines) { - if (sourceLine.matchingLine != null) continue // Skip lines that already have matches - - var bestMatch: LineRecord? = null - var bestDistance = Int.MAX_VALUE - var bestProximity = Int.MAX_VALUE - - for (patchLine in patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }) { - if (patchLine.matchingLine != null) continue // Skip lines that already have matches - - // Calculate the Levenshtein distance between unmatched source and patch lines - val distance = levenshteinDistance.apply(normalizeLine(sourceLine.line), normalizeLine(patchLine.line)) - if (distance <= maxDistance) { - // Consider proximity to established links as a secondary factor - val proximity = calculateProximityDistance(sourceLine, patchLine) - if (distance < bestDistance || (distance == bestDistance && proximity < bestProximity)) { - if (distance < bestDistance) { - bestMatch = patchLine - bestDistance = distance - bestProximity = proximity - } - } - } ++ public width: number; ++ public height: number; + """.trimIndent() + val expected = """ +import {Scene} from './Scene'; - // Establish the best match found, if any - if (bestMatch != null) { - sourceLine.matchingLine = bestMatch - bestMatch.matchingLine = sourceLine - } - } - } +export class Game { + private currentScene: Scene | null = null; + private isRunning: boolean = false; + private lastTimestamp: number = 0; + + constructor(private canvas: HTMLCanvasElement) { + this.width = canvas.width; + this.height = canvas.height; } - /** - * Calculates the proximity distance between a source line and a patch line based on their distance to the nearest established link. - * @param sourceLine The source line. - * @param patchLine The patch line. - * @return The proximity distance. - */ - private fun calculateProximityDistance(sourceLine: LineRecord, patchLine: LineRecord): Int { - // Find the nearest established link in both directions for source and patch lines - var sourceDistancePrev = 0 - var sourceDistanceNext = 0 - var patchDistancePrev = 0 - var patchDistanceNext = 0 - - var currentSourceLine = sourceLine.previousLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistancePrev++ - currentSourceLine = currentSourceLine.previousLine - } + public width: number; + public height: number; - currentSourceLine = sourceLine.nextLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistanceNext++ - currentSourceLine = currentSourceLine.nextLine - } + public start(): void { + this.isRunning = true; + this.lastTimestamp = performance.now(); + requestAnimationFrame(this.gameLoop.bind(this)); + } - var currentPatchLine = patchLine.previousLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistancePrev++ - currentPatchLine = currentPatchLine.previousLine - } + public stop(): void { + this.isRunning = false; + } - currentPatchLine = patchLine.nextLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistanceNext++ - currentPatchLine = currentPatchLine.nextLine - } + public setScene(scene: Scene): void { + this.currentScene = scene; + } + + private gameLoop(timestamp: number): void { + if (!this.isRunning) return; - // Calculate the total proximity distance as the sum of minimum distances in each direction - return minOf(sourceDistancePrev, patchDistancePrev) + minOf(sourceDistanceNext, patchDistanceNext) + const deltaTime = (timestamp - this.lastTimestamp) / 1000; // Convert to seconds + this.lastTimestamp = timestamp; + + this.update(deltaTime); + this.render(); + + requestAnimationFrame(this.gameLoop.bind(this)); } - /** - * Parses the given text into a list of line records. - * @param text The text to parse. - * @return The list of line records. - */ - private fun parseLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord(index, line) - }) - - /** - * Sets the previous and next line links for a list of line records. - * @param list The list of line records. - * @return The list with links set. - */ - private fun setLinks(list: List): List { - for (i in 0 until list.size) { - list[i].previousLine = if (i > 0) list[i - 1] else null - list[i].nextLine = if (i < list.size - 1) list[i + 1] else null + private update(deltaTime: number): void { + if (this.currentScene) { + this.currentScene.update(deltaTime); } - return list } - /** - * Parses the patch text into a list of line records, identifying the type of each line (ADD, DELETE, CONTEXT). - * @param text The patch text to parse. - * @return The list of line records with types set. - */ - private fun parsePatchLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord( - index = index, line = line.let { - when { - it.trimStart().startsWith("+") -> it.trimStart().substring(1) - it.trimStart().startsWith("-") -> it.trimStart().substring(1) - else -> it - } - }, type = when { - line.startsWith("+") -> LineType.ADD - line.startsWith("-") -> LineType.DELETE - else -> LineType.CONTEXT - } - ) - }) + private render(): void { + const ctx = this.canvas.getContext('2d'); + if (!ctx) return; -} + // Clear the canvas + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + if (this.currentScene) { + this.currentScene.render(ctx); + } + } +} """.trimIndent() - val expected = """ -package com.simiacryptus.diff + val result = IterativePatchUtil.patch(source, patch) + Assertions.assertEquals( + expected.replace("\r\n", "\n"),//.replace("\\s{2,}".toRegex(), " "), + result.replace("\r\n", "\n")//.replace("\\s{2,}".toRegex(), " ") + ) + } + + @Test + fun testFromData4() { + val source = """ +import {ColorMixer} from '@/systems/ColorMixer'; -import org.apache.commons.text.similarity.LevenshteinDistance - -object IterativePatchUtil { - - enum class LineType { CONTEXT, ADD, DELETE } - class LineRecord( - val index: Int, - val line: String, - var previousLine: LineRecord? = null, - var nextLine: LineRecord? = null, - var matchingLine: LineRecord? = null, - var type: LineType = LineType.CONTEXT - ) { - override fun toString(): String { - val sb = StringBuilder() - when (type) { - LineType.CONTEXT -> sb.append(" ") - LineType.ADD -> sb.append("+") - LineType.DELETE -> sb.append("-") - } - sb.append(" ") - sb.append(line) - return sb.toString() - } - } - - /** - * Normalizes a line by removing all whitespace. - * @param line The line to normalize. - * @return The normalized line. - */ - private fun normalizeLine(line: String): String { - return line.replace("\\s".toRegex(), "") - } - - /** - * Applies a patch to the given source text. - * @param source The original text. - * @param patch The patch to apply. - * @return The text after the patch has been applied. - */ - fun patch(source: String, patch: String): String { - // Parse the source and patch texts into lists of line records - val sourceLines = parseLines(source) - val patchLines = parsePatchLines(patch) - - // Step 1: Link all unique lines in the source and patch that match exactly - linkUniqueMatchingLines(sourceLines, patchLines) - - // Step 2: Link all exact matches in the source and patch which are adjacent to established links - linkAdjacentMatchingLines(sourceLines) - - // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links. - // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found. -// linkByLevenshteinDistance(sourceLines, patchLines) - - // Generate the patched text using the established links - return generatePatchedTextUsingLinks(sourceLines, patchLines) - } - - /** - * Generates the final patched text using the links established between the source and patch lines. - * @param sourceLines The source lines with established links. - * @param patchLines The patch lines with established links. - * @return The final patched text. - */ - private fun generatePatchedTextUsingLinks(sourceLines: List, patchLines: List): String { - val patchedTextBuilder = StringBuilder() - val sourceLineBuffer = sourceLines.toMutableList() - - // Add any leading 'add' lines from the patch - val patchLines = patchLines.toMutableList() - while (patchLines.firstOrNull()?.type == LineType.ADD) { - patchedTextBuilder.appendLine(patchLines.removeFirst().line) - } - - // Process the rest of the lines - while (sourceLineBuffer.isNotEmpty()) { - // Copy all lines until the next matched line - val codeLine = sourceLineBuffer.removeFirst() - when { - codeLine.matchingLine == null -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.DELETE -> null // Skip adding the line - codeLine.matchingLine!!.type == LineType.CONTEXT -> patchedTextBuilder.appendLine(codeLine.line) - codeLine.matchingLine!!.type == LineType.ADD -> throw IllegalStateException("ADD line is matched to source line") - } - - // Add lines marked as ADD in the patch following the current matched line - var nextPatchLine = codeLine.matchingLine?.nextLine - while (nextPatchLine != null && nextPatchLine.matchingLine == null) { - when(nextPatchLine.type) { - LineType.ADD -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.CONTEXT -> patchedTextBuilder.appendLine(nextPatchLine.line) - LineType.DELETE -> null // Skip adding the line - } - nextPatchLine = nextPatchLine.nextLine - } - } - return patchedTextBuilder.toString().trimEnd() - } - - /** - * Links lines between the source and the patch that are unique and match exactly. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) { - val sourceLineMap = sourceLines.groupBy { normalizeLine(it.line) } - val patchLineMap = patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }.groupBy { normalizeLine(it.line) } - - sourceLineMap.keys.intersect(patchLineMap.keys).forEach { key -> - val sourceLine = sourceLineMap[key]?.singleOrNull() - val patchLine = patchLineMap[key]?.singleOrNull() - if (sourceLine != null && patchLine != null) { - sourceLine.matchingLine = patchLine - patchLine.matchingLine = sourceLine - } - } - } - - /** - * Links lines that are adjacent to already linked lines and match exactly. - * @param sourceLines The source lines with some established links. - */ - private fun linkAdjacentMatchingLines(sourceLines: List) { - var foundMatch = true - while (foundMatch) { - foundMatch = false - for (sourceLine in sourceLines) { - val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line - - // Check the previous line for a potential match - if (sourceLine.previousLine != null && patchLine.previousLine != null) { - val sourcePrev = sourceLine.previousLine!! - var patchPrev = patchLine.previousLine!! - while (patchPrev.type == LineType.ADD && patchPrev.previousLine != null) { - patchPrev = patchPrev.previousLine!! - } - if (normalizeLine(sourcePrev.line) == normalizeLine(patchPrev.line) && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) { - sourcePrev.matchingLine = patchPrev - patchPrev.matchingLine = sourcePrev - foundMatch = true - } - } - - // Check the next line for a potential match - if (sourceLine.nextLine != null && patchLine.nextLine != null) { - val sourceNext = sourceLine.nextLine!! - var patchNext = patchLine.nextLine!! - while (patchNext.type == LineType.ADD && patchNext.nextLine != null) { - patchNext = patchNext.nextLine!! - } - if (normalizeLine(sourceNext.line) == normalizeLine(patchNext.line) && sourceNext.matchingLine == null && patchNext.matchingLine == null) { - sourceNext.matchingLine = patchNext - patchNext.matchingLine = sourceNext - foundMatch = true - } - } - } - } - } - - /** - * Establishes links between source and patch lines based on the Levenshtein distance and proximity to already established links. - * @param sourceLines The source lines. - * @param patchLines The patch lines. - */ - private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) { - val levenshteinDistance = LevenshteinDistance() - val maxDistance = (sourceLines.size + patchLines.size) / 10 // Increase max distance to allow more flexibility - - // Iterate over source lines to find potential matches in the patch lines - for (sourceLine in sourceLines) { - if (sourceLine.matchingLine != null) continue // Skip lines that already have matches - - var bestMatch: LineRecord? = null - var bestDistance = Int.MAX_VALUE - var bestProximity = Int.MAX_VALUE - - for (patchLine in patchLines.filter { - when (it.type) { - LineType.ADD -> false // ADD lines are not matched to source lines - else -> true - } - }) { - if (patchLine.matchingLine != null) continue // Skip lines that already have matches - - // Calculate the Levenshtein distance between unmatched source and patch lines - val distance = levenshteinDistance.apply(normalizeLine(sourceLine.line), normalizeLine(patchLine.line)) - if (distance <= maxDistance) { - // Consider proximity to established links as a secondary factor - val proximity = calculateProximityDistance(sourceLine, patchLine) - if (distance < bestDistance || (distance == bestDistance && proximity < bestProximity)) { - if (distance < bestDistance) { - bestMatch = patchLine - bestDistance = distance - bestProximity = proximity - } - } - } - - // Establish the best match found, if any - if (bestMatch != null) { - sourceLine.matchingLine = bestMatch - bestMatch.matchingLine = sourceLine - } - } - } - } - - /** - * Calculates the proximity distance between a source line and a patch line based on their distance to the nearest established link. - * @param sourceLine The source line. - * @param patchLine The patch line. - * @return The proximity distance. - */ - private fun calculateProximityDistance(sourceLine: LineRecord, patchLine: LineRecord): Int { - // Find the nearest established link in both directions for source and patch lines - var sourceDistancePrev = 0 - var sourceDistanceNext = 0 - var patchDistancePrev = 0 - var patchDistanceNext = 0 - - var currentSourceLine = sourceLine.previousLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistancePrev++ - currentSourceLine = currentSourceLine.previousLine - } - - currentSourceLine = sourceLine.nextLine - while (currentSourceLine != null) { - if (currentSourceLine.matchingLine != null) break - sourceDistanceNext++ - currentSourceLine = currentSourceLine.nextLine - } - - var currentPatchLine = patchLine.previousLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistancePrev++ - currentPatchLine = currentPatchLine.previousLine - } - - currentPatchLine = patchLine.nextLine - while (currentPatchLine != null) { - if (currentPatchLine.matchingLine != null) break - patchDistanceNext++ - currentPatchLine = currentPatchLine.nextLine - } - - // Calculate the total proximity distance as the sum of minimum distances in each direction - return minOf(sourceDistancePrev, patchDistancePrev) + minOf(sourceDistanceNext, patchDistanceNext) - } - - /** - * Parses the given text into a list of line records. - * @param text The text to parse. - * @return The list of line records. - */ - private fun parseLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord(index, line) - }) - - /** - * Sets the previous and next line links for a list of line records. - * @param list The list of line records. - * @return The list with links set. - */ - private fun setLinks(list: List): List { - for (i in 0 until list.size) { - list[i].previousLine = if (i > 0) list[i - 1] else null - list[i].nextLine = if (i < list.size - 1) list[i + 1] else null - } - return list - } - - /** - * Parses the patch text into a list of line records, identifying the type of each line (ADD, DELETE, CONTEXT). - * @param text The patch text to parse. - * @return The list of line records with types set. - */ - private fun parsePatchLines(text: String) = setLinks(text.lines().mapIndexed { index, line -> - LineRecord( - index = index, line = line.let { - when { - it.trimStart().startsWith("+") -> it.trimStart().substring(1) - it.trimStart().startsWith("-") -> it.trimStart().substring(1) - else -> it - } - }, type = when { - line.startsWith("+") -> LineType.ADD - line.startsWith("-") -> LineType.DELETE - else -> LineType.CONTEXT - } - ) - }) +describe('ColorMixer', () => { + const colorMixer = new ColorMixer(); -} + test('mixing red and green should result in yellow', () => { + let colorMixer: ColorMixer; + + beforeEach(() => { + colorMixer = new ColorMixer(); + }); + + test('mixing red and green should produce yellow', () => { + const result = colorMixer.mixColors('red', 'green'); + expect(result).toBe('red'); // Temporary expectation based on placeholder implementation + }); + + test('mixing blue and yellow should result in green', () => { + const result = colorMixer.mixColors('blue', 'yellow'); + expect(result).toBe('blue'); // Temporary expectation based on placeholder implementation + }); + + test('mixing red, green, and blue should result in white', () => { + const result = colorMixer.mixColors('red', 'green', 'blue'); + expect(result).toBe('red'); // Temporary expectation based on placeholder implementation + }); + }); + """.trimIndent() + val patch = """ + import {ColorMixer} from '@/systems/ColorMixer'; + + describe('ColorMixer', () => { +- const colorMixer = new ColorMixer(); ++ let colorMixer: ColorMixer; + + beforeEach(() => { + colorMixer = new ColorMixer(); + }); + + test('mixing red and green should produce yellow', () => { + const result = colorMixer.mixColors('red', 'green'); +- expect(result).toBe('red'); // Temporary expectation based on placeholder implementation ++ expect(result).toBe('#ffff00'); // Yellow in hex + }); + + test('mixing blue and yellow should result in green', () => { + const result = colorMixer.mixColors('blue', 'yellow'); +- expect(result).toBe('blue'); // Temporary expectation based on placeholder implementation ++ expect(result).toBe('#80ff80'); // Light green in hex + }); + + test('mixing red, green, and blue should result in white', () => { + const result = colorMixer.mixColors('red', 'green', 'blue'); +- expect(result).toBe('red'); // Temporary expectation based on placeholder implementation ++ expect(result).toBe('#ffffff'); // White in hex + }); + }); """.trimIndent() val result = IterativePatchUtil.patch(source, patch) + val expected = """ +import {ColorMixer} from '@/systems/ColorMixer'; + +describe('ColorMixer', () => { + const colorMixer = new ColorMixer(); + let colorMixer: ColorMixer; + + beforeEach(() => { + colorMixer = new ColorMixer(); + }); + + test('mixing red and green should produce yellow', () => { + const result = colorMixer.mixColors('red', 'green'); + expect(result).toBe('#ffff00'); // Yellow in hex + }); + + test('mixing blue and yellow should result in green', () => { + const result = colorMixer.mixColors('blue', 'yellow'); + expect(result).toBe('#80ff80'); // Light green in hex + }); + + test('mixing red, green, and blue should result in white', () => { + const result = colorMixer.mixColors('red', 'green', 'blue'); + expect(result).toBe('#ffffff'); // White in hex + }); +}); + """.trimIndent() Assertions.assertEquals( - expected.replace("\r\n", "\n").replace("[ \\t]{1,}".toRegex(), " "), - result.replace("\r\n", "\n").replace("[ \\t]{1,}".toRegex(), " ") + expected.replace("\r\n", "\n"),//.replace("\\s{2,}".toRegex(), " "), + result.replace("\r\n", "\n")//.replace("\\s{2,}".toRegex(), " ") ) } +*/ + } \ No newline at end of file