From 8184cf771df8bd02050be6ecd87e3775bc532c7e Mon Sep 17 00:00:00 2001 From: Martin Hoke Date: Mon, 21 Jul 2025 11:35:35 +0200 Subject: [PATCH] broadcast --- .gitea/workflows/build.yml | 2 +- .gitignore | 3 - .idea/.gitignore | 8 - .idea/.name | 1 - .idea/compiler.xml | 6 - .idea/discord.xml | 14 - .idea/gradle.xml | 16 - .idea/highlightedFiles.xml | 17 - .idea/misc.xml | 8 - .idea/modules.xml | 8 - .idea/modules/OllamaChat.main.iml | 14 - .idea/vcs.xml | 6 - README.md | 138 +++++-- build.gradle | 6 +- gradlew | 0 src/main/java/com/ollamachat/AIService.java | 126 ++++++- .../com/ollamachat/ChatHistoryManager.java | 36 +- .../java/com/ollamachat/DatabaseManager.java | 284 ++++++++++++-- .../java/com/ollamachat/DependencyLoader.java | 114 ++++++ src/main/java/com/ollamachat/Ollamachat.java | 352 ------------------ .../java/com/ollamachat/ProgressManager.java | 104 ++++++ .../ollamachat/chat/ChatTriggerHandler.java | 164 ++++++++ .../chat/SuggestedResponseHandler.java | 181 +++++++++ .../com/ollamachat/command/AIChatCommand.java | 48 +++ .../ollamachat/command/OllamaChatCommand.java | 265 +++++++++++++ .../command/OllamaChatTabCompleter.java | 143 +++++++ .../com/ollamachat/core/ConfigManager.java | 325 ++++++++++++++++ .../java/com/ollamachat/core/Ollamachat.java | 112 ++++++ src/main/resources/config.yml | 57 ++- src/main/resources/lang/en.lang | 18 - src/main/resources/lang/en_us.json | 51 +++ src/main/resources/lang/zh_cn.json | 52 +++ src/main/resources/lang/zh_cn.lang | 18 - src/main/resources/plugin.yml | 47 ++- 34 files changed, 2171 insertions(+), 573 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/discord.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/highlightedFiles.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/modules/OllamaChat.main.iml delete mode 100644 .idea/vcs.xml mode change 100755 => 100644 gradlew create mode 100644 src/main/java/com/ollamachat/DependencyLoader.java delete mode 100644 src/main/java/com/ollamachat/Ollamachat.java create mode 100644 src/main/java/com/ollamachat/ProgressManager.java create mode 100644 src/main/java/com/ollamachat/chat/ChatTriggerHandler.java create mode 100644 src/main/java/com/ollamachat/chat/SuggestedResponseHandler.java create mode 100644 src/main/java/com/ollamachat/command/AIChatCommand.java create mode 100644 src/main/java/com/ollamachat/command/OllamaChatCommand.java create mode 100644 src/main/java/com/ollamachat/command/OllamaChatTabCompleter.java create mode 100644 src/main/java/com/ollamachat/core/ConfigManager.java create mode 100644 src/main/java/com/ollamachat/core/Ollamachat.java delete mode 100644 src/main/resources/lang/en.lang create mode 100644 src/main/resources/lang/en_us.json create mode 100644 src/main/resources/lang/zh_cn.json delete mode 100644 src/main/resources/lang/zh_cn.lang diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 76621b5..28211c5 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -24,4 +24,4 @@ jobs: ./gradlew build && ls -lah && ls */ -lah && ls */* -lah&& ls */*/* -lah - name: Build | publish jar run: | - curl --insecure --user username:mypass -T build/libs/OllamaChat-1.0.2.jar ftp://192.168.10.133:/ \ No newline at end of file + curl --insecure --user username:mypass -T build/libs/OllamaChat-1.1.4.jar ftp://192.168.10.133:/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78090fc..524f096 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,3 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* - -build -.gradle \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 3566ed5..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -OllamaChat \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b86273d..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 6b01108..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ce1c62c..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/highlightedFiles.xml b/.idea/highlightedFiles.xml deleted file mode 100644 index f9d3c5e..0000000 --- a/.idea/highlightedFiles.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7c47dd4..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index dc14fc0..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/OllamaChat.main.iml b/.idea/modules/OllamaChat.main.iml deleted file mode 100644 index bbeeb3e..0000000 --- a/.idea/modules/OllamaChat.main.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - PAPER - ADVENTURE - - 1 - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 927b30f..d45560a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,101 @@ -![Version](https://img.shields.io/badge/version-1.0.2-blue) - -# Ollama-Chat - +![Version](https://img.shields.io/badge/version-1.1.4-blue) +# OllamaChat [![Download](https://github.com/gabrielvicenteYT/modrinth-icons/blob/main/Branding/Badge/badge-dark.svg)](https://modrinth.com/plugin/ollama-chat) - ## Overview -**Ollama-Chat** is a cutting-edge Minecraft plugin that brings the power of Ollama and other AI models directly into your Minecraft world. This plugin enables players to interact with AI in real-time, creating a unique and immersive gameplay experience. Whether you want to chat with an AI companion, ask questions, or simply explore the capabilities of AI, Ollama-Chat makes it possible within the Minecraft universe. +**OllamaChat** is a cutting-edge Minecraft plugin that integrates Ollama and OpenAI-class APIs, enabling real-time AI interactions, multi-language support, and advanced prompt and conversation management for immersive in-game experiences. ## Features -- **AI-Powered Conversations**: Communicate with AI entities in Minecraft by sending messages prefixed with `@bot`. The AI will respond intelligently, providing a dynamic and engaging interaction. -- **Ollama Integration**: Leverage the advanced capabilities of Ollama to enhance your Minecraft experience. -- **Multi-Language Support**: Supports multiple languages (e.g., English, Simplified Chinese) through language files in the `lang` folder. -- **Toggle AI Services**: Enable or disable AI services dynamically using the `/ollamachat toggle ` command. -- **Simple Commands**: Use the `/ollamachat reload` command to reload the plugin configuration instantly, ensuring seamless updates without server restarts. +- **AI-Powered Conversations**: Chat with AI using `@bot` or `@ai` prefixes for dynamic, intelligent responses. +- **Ollama & OpenAI Integration**: Leverage advanced AI models to enhance your Minecraft experience. +- **Multi-Language Support**: Supports multiple languages (e.g., English, Simplified Chinese) via `lang` folder files. +- **Toggle AI Services**: Enable/disable AI services with `/ollamachat toggle `. +- **Prompt Management**: Create, delete, list, select, or clear custom prompts to tailor AI interactions. +- **Conversation Management**: Start, switch, delete, or view player-specific conversations with AI entities. +- **Smart Response Suggestions**: Generate configurable, clickable follow-up suggestions with hover text and rate limiting. +- **Tab Completion**: Enhanced usability with Tab completion for `/ollamachat` subcommands. +- **Progress Display**: Visual status bar for prompt answer generation (0% to 100%). +- **Configurable Settings**: Customize suggestion count, prompt templates, preset responses, and model toggles. ## Usage ### Chatting with AI -To interact with the AI, simply type `@bot` followed by your message in the Minecraft chat. The AI will process your input and respond accordingly. +Type `@bot` or `@ai` followed by your message in Minecraft chat to interact with the AI. **Example:** ``` -@bot What is the weather like today? +@bot What's the best way to build a castle? ``` ### Commands -- **/ollamachat reload**: Reloads the plugin configuration, including language files and AI settings. -- **/ollamachat toggle **: Enables or disables the specified AI service. -- **/aichat **: Interacts with other AI services (e.g., OpenAI). +- **/ollamachat reload**: Reloads plugin configuration and language files (`ollamachat.reload`). +- **/ollamachat toggle **: Enables/disables specified AI service (`ollamachat.toggle`). +- **/aichat **: Interacts with other AI services (`ollamachat.use`). +- **/ollamachat prompt set **: Creates a new prompt (`ollamachat.prompt.set`). +- **/ollamachat prompt delete **: Deletes a prompt (`ollamachat.prompt.delete`). +- **/ollamachat prompt list**: Lists all prompts (`ollamachat.prompt.list`). +- **/ollamachat prompt select **: Sets default prompt (`ollamachat.prompt.select`). +- **/ollamachat prompt clear**: Resets default prompt (`ollamachat.prompt.select`). +- **/ollamachat conversation new **: Starts a new conversation (`ollamachat.conversation.new`). +- **/ollamachat conversation select **: Switches conversations (`ollamachat.conversation.select`). +- **/ollamachat conversation delete **: Deletes a conversation (`ollamachat.conversation.delete`). +- **/ollamachat conversation list **: Lists conversations for an AI (`ollamachat.conversation.list`). +- **/ollamachat suggests toggle**: Toggles suggested responses (`ollamachat.suggests.toggle`). + +### Permissions + +| Command | Permission | Description | +|---------|------------|-------------| +| `/ollamachat reload` | `ollamachat.reload` | Reloads plugin configuration. | +| `/ollamachat toggle ` | `ollamachat.toggle` | Toggles specified AI service. | +| `/aichat ` | `ollamachat.use` | Sends a message to specified AI. | +| `/ollamachat prompt set ` | `ollamachat.prompt.set` | Creates and saves a new prompt. | +| `/ollamachat prompt delete ` | `ollamachat.prompt.delete` | Deletes a specified prompt. | +| `/ollamachat prompt list` | `ollamachat.prompt.list` | Lists all prompts and current default. | +| `/ollamachat prompt select ` | `ollamachat.prompt.select` | Sets a prompt as default. | +| `/ollamachat prompt clear` | `ollamachat.prompt.select` | Resets default prompt. | +| `/ollamachat conversation new ` | `ollamachat.conversation.new` | Starts a new conversation. | +| `/ollamachat conversation select ` | `ollamachat.conversation.select` | Switches to an existing conversation. | +| `/ollamachat conversation delete ` | `ollamachat.conversation.delete` | Deletes a conversation. | +| `/ollamachat conversation list ` | `ollamachat.conversation.list` | Lists all conversations for an AI. | +| `/ollamachat suggests toggle` | `ollamachat.suggests.toggle` | Toggles suggested responses. | +| `/ollamachat suggests-presets toggle` | `ollamachat.suggests-presets.toggle` | Toggles preset suggested responses. | **Example:** ``` -/aichat openai Tell me a joke +/aichat ollama Tell me about Redstone +/ollamachat prompt set creativePrompt "Act as a creative Minecraft builder" +/ollamachat suggests toggle ``` ## Installation 1. **Download the Plugin**: Obtain the latest version of **Ollama-Chat** from the [official repository](https://github.com/mcraftbbs/Ollama-Chat). -2. **Install the Plugin**: Place the downloaded `.jar` file into the `plugins` folder of your Minecraft server. -3. **Configure the Plugin**: Modify the `config.yml` file to customize AI settings. -4. **Reload the Plugin**: Use the `/ollamachat reload` command to apply any configuration changes. +2. **Install**: Place the `.jar` file in your server's `plugins` folder. +3. **Configure**: Edit `config.yml` to customize AI settings, prompts, and suggestions. +4. **Reload**: Use `/ollamachat reload` to apply changes. ## Configuration -The plugin's configuration file (`config.yml`) allows you to customize various aspects of the AI interactions. +Customize AI interactions via `config.yml`: -Example `config.yml`: ```yaml # Ollama API ollama-api-url: "http://localhost:11434/api/generate" model: "llama3" ollama-enabled: true +# Streaming settings +stream-settings: + enabled: true + # Chat -trigger-prefix: "@bot " -response-prefix: "§b[AI] §r" +trigger-prefixes: + - "@bot" + - "@ai" # Length max-response-length: 500 @@ -67,7 +104,48 @@ max-response-length: 500 max-history: 5 # Language Settings -language: "en" # Default language (en or zh_cn) +language: "en_us" + +# Progress Display Settings +progress-display: + enabled: true + type: "bossbar" + color: "BLUE" + style: "SOLID" + update-interval: 1 + +# Suggested Response +suggested-responses-enabled: false +suggested-response-models: + - "llama3" +suggested-response-count: 3 +suggested-response-prompt: "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3" +suggested-response-presets: + - "I see what you mean." + - "That's interesting!" + - "Tell me more about that." +suggested-response-presets-enabled: false +suggested-response-model-toggles: + - "llama3" +suggested-response-cooldown: 10 + +# Database (for mysql, set database.type: mysql.) +database: + type: sqlite + mysql: + host: localhost + port: 3306 + database: ollamachat + username: root + password: "" + +# Default prompt +default-prompt: "" + +# Custom prompts +prompts: +# friendly: "You are a friendly assistant who responds in a cheerful tone." +# formal: "You are a professional assistant who responds formally." # Other AI Configurations other-ai-configs: @@ -75,21 +153,21 @@ other-ai-configs: api-url: "https://api.openai.com/v1/chat/completions" api-key: "your-openai-api-key" model: "gpt-4" - enabled: true + enabled: false + messages-format: true ``` ## Contributing -We welcome contributions from the community to improve **Ollama-Chat**! If you have ideas, bug reports, or feature requests, please open an issue or submit a pull request on our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat). +We welcome contributions! Submit issues or pull requests on our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat). ## License -**Ollama-Chat** is licensed under the MIT License. For more details, see the [LICENSE](LICENSE) file. +Licensed under the MIT License. See [LICENSE](https://github.com/mcraftbbs/Ollama-Chat?tab=MIT-1-ov-file). ## Support -For assistance, questions, or feedback, please visit our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat) or join our [community server](https://chat.sarskin.cn/invite/iHgI6LTX). +For help, visit our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat) or join our [community server](https://chat.sarskin.cn/invite/iHgI6LTX). ---- **Note**: **Ollama-Chat** is actively developed, with new features and improvements being added regularly. Stay tuned for updates! diff --git a/build.gradle b/build.gradle index 723ac50..75f6a97 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'com' -version = '1.0.2' +version = '1.1.4' repositories { mavenCentral() @@ -19,6 +19,8 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") + compileOnly("mysql:mysql-connector-java:8.0.33") + compileOnly("org.xerial:sqlite-jdbc:3.46.0.0") } def targetJavaVersion = 21 @@ -47,3 +49,5 @@ processResources { expand props } } + + diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/src/main/java/com/ollamachat/AIService.java b/src/main/java/com/ollamachat/AIService.java index f54c76e..c198fb0 100644 --- a/src/main/java/com/ollamachat/AIService.java +++ b/src/main/java/com/ollamachat/AIService.java @@ -1,15 +1,15 @@ package com.ollamachat; import com.google.gson.Gson; -import org.bukkit.Bukkit; +import com.google.gson.JsonObject; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.concurrent.CompletableFuture; public class AIService { @@ -22,14 +22,28 @@ public class AIService { this.gson = new Gson(); } - public CompletableFuture sendRequest(String apiUrl, String apiKey, String model, String prompt) { + public CompletableFuture sendRequest(String apiUrl, String apiKey, String model, String prompt, boolean isMessagesFormat) { return CompletableFuture.supplyAsync(() -> { try { - Map requestBody = Map.of( - "model", model, - "prompt", prompt, - "stream", false - ); + Map requestBody; + if (isMessagesFormat) { + requestBody = Map.of( + "model", model, + "messages", List.of( + Map.of( + "role", "user", + "content", prompt + ) + ), + "stream", false + ); + } else { + requestBody = Map.of( + "model", model, + "prompt", prompt, + "stream", false + ); + } String jsonRequest = gson.toJson(requestBody); @@ -38,6 +52,10 @@ public class AIService { .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonRequest)); + if (apiKey != null && !apiKey.isEmpty()) { + requestBuilder.header("Authorization", "Bearer " + apiKey); + } + HttpRequest request = requestBuilder.build(); HttpResponse response = httpClient.send( @@ -46,18 +64,102 @@ public class AIService { ); if (response.statusCode() == 200) { - Bukkit.getConsoleSender().sendMessage("AI API Response: " + response.body()); return response.body(); } else { - Bukkit.getConsoleSender().sendMessage("AI API Response-ERR: " + response.body()); throw new RuntimeException("AI API Error: " + response.body()); } } catch (Exception e) { - Bukkit.getConsoleSender().sendMessage("AI API Response-ERR: " + e.getMessage()); throw new RuntimeException("Failed to get response from AI: " + e.getMessage(), e); } }); } + + public CompletableFuture sendStreamingRequest(String apiUrl, String apiKey, String model, String prompt, Consumer responseConsumer, boolean isMessagesFormat) { + return CompletableFuture.runAsync(() -> { + try { + Map requestBody; + if (isMessagesFormat) { + requestBody = Map.of( + "model", model, + "messages", List.of( + Map.of( + "role", "user", + "content", prompt + ) + ), + "stream", true + ); + } else { + requestBody = Map.of( + "model", model, + "prompt", prompt, + "stream", true + ); + } + + String jsonRequest = gson.toJson(requestBody); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonRequest)); + + if (apiKey != null && !apiKey.isEmpty()) { + requestBuilder.header("Authorization", "Bearer " + apiKey); + } + + HttpRequest request = requestBuilder.build(); + + HttpResponse response = httpClient.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() == 200) { + StringBuilder buffer = new StringBuilder(); + int minBufferLength = 50; // Minimum length before sending + String[] lines = response.body().split("\n"); + for (String line : lines) { + if (!line.trim().isEmpty()) { + if (isMessagesFormat && line.startsWith("data: ")) { + String jsonData = line.substring(6); // Remove "data: " prefix + if (jsonData.equals("[DONE]")) continue; + JsonObject json = gson.fromJson(jsonData, JsonObject.class); + if (json.has("choices")) { + String partialResponse = json.getAsJsonArray("choices") + .get(0).getAsJsonObject() + .get("delta").getAsJsonObject() + .get("content").getAsString(); + buffer.append(partialResponse); + } + } else if (!isMessagesFormat) { + JsonObject json = gson.fromJson(line, JsonObject.class); + if (json.has("response")) { + String partialResponse = json.get("response").getAsString(); + buffer.append(partialResponse); + } + } + + // Check if buffer ends with a sentence boundary or is long enough + String currentBuffer = buffer.toString(); + if (currentBuffer.endsWith(".") || currentBuffer.endsWith("?") || + currentBuffer.endsWith("!") || currentBuffer.length() >= minBufferLength) { + responseConsumer.accept(currentBuffer); + buffer.setLength(0); // Clear buffer + } + } + } + // Send any remaining content in the buffer + if (buffer.length() > 0) { + responseConsumer.accept(buffer.toString()); + } + } else { + throw new RuntimeException("AI API Error: " + response.body()); + } + } catch (Exception e) { + throw new RuntimeException("Failed to get streaming response from AI: " + e.getMessage(), e); + } + }); + } } - diff --git a/src/main/java/com/ollamachat/ChatHistoryManager.java b/src/main/java/com/ollamachat/ChatHistoryManager.java index c263d83..44761b1 100644 --- a/src/main/java/com/ollamachat/ChatHistoryManager.java +++ b/src/main/java/com/ollamachat/ChatHistoryManager.java @@ -2,6 +2,8 @@ package com.ollamachat; import org.bukkit.entity.Player; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; public class ChatHistoryManager { @@ -17,11 +19,35 @@ public class ChatHistoryManager { databaseManager.savePlayerInfo(player.getUniqueId(), player.getName()); } - public void saveChatHistory(UUID playerUuid, String aiModel, String prompt, String response) { - databaseManager.saveChatHistory(playerUuid, aiModel, prompt, response); + public String createConversation(UUID playerUuid, String aiModel, String convName) { + return databaseManager.createConversation(playerUuid, aiModel, convName); } - public String getChatHistory(UUID playerUuid, String aiModel) { - return databaseManager.getChatHistory(playerUuid, aiModel, maxHistory); + public boolean conversationExistsByName(UUID playerUuid, String aiModel, String convName) { + return databaseManager.conversationExistsByName(playerUuid, aiModel, convName); } -} + + public String getConversationId(UUID playerUuid, String aiModel, String convName) { + return databaseManager.getConversationId(playerUuid, aiModel, convName); + } + + public boolean conversationExists(UUID playerUuid, String aiModel, String convId) { + return databaseManager.conversationExists(playerUuid, aiModel, convId); + } + + public boolean deleteConversation(UUID playerUuid, String aiModel, String convId) { + return databaseManager.deleteConversation(playerUuid, aiModel, convId); + } + + public Map listConversations(UUID playerUuid, String aiModel) { + return databaseManager.listConversations(playerUuid, aiModel); + } + + public void saveChatHistory(UUID playerUuid, String aiModel, String conversationId, String prompt, String response) { + databaseManager.saveChatHistory(playerUuid, aiModel, conversationId, prompt, response); + } + + public String getChatHistory(UUID playerUuid, String aiModel, String conversationId) { + return databaseManager.getChatHistory(playerUuid, aiModel, conversationId, maxHistory); + } +} \ No newline at end of file diff --git a/src/main/java/com/ollamachat/DatabaseManager.java b/src/main/java/com/ollamachat/DatabaseManager.java index 9d7fe1d..401741f 100644 --- a/src/main/java/com/ollamachat/DatabaseManager.java +++ b/src/main/java/com/ollamachat/DatabaseManager.java @@ -1,81 +1,308 @@ + package com.ollamachat; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + import java.sql.*; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.logging.Logger; public class DatabaseManager { - private Connection connection; + private final JavaPlugin plugin; + private final Logger logger; + private String databaseType; // "sqlite" or "mysql" + private Connection sqliteConnection; // For SQLite, maintain a single connection + private final ClassLoader dependencyClassLoader; + private String mysqlUrl; + private String mysqlUsername; + private String mysqlPassword; - public DatabaseManager() { + public DatabaseManager(JavaPlugin plugin, ClassLoader dependencyClassLoader) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + this.dependencyClassLoader = dependencyClassLoader; initializeDatabase(); } private void initializeDatabase() { + FileConfiguration config = plugin.getConfig(); + databaseType = config.getString("database.type", "sqlite").toLowerCase(); + try { - Class.forName("org.sqlite.JDBC"); - connection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db"); + if (databaseType.equals("mysql")) { + initializeMySQL(); + } else { + initializeSQLite(); + } createTables(); } catch (Exception e) { + logger.severe("Failed to initialize database: " + e.getMessage()); e.printStackTrace(); + throw new RuntimeException("Database initialization failed", e); + } + } + + private void initializeSQLite() { + try { + // Use the dependency class loader to load SQLite driver + Class.forName("org.sqlite.JDBC", true, dependencyClassLoader); + } catch (ClassNotFoundException e) { + logger.severe("SQLite JDBC driver not found. Ensure 'sqlite-jdbc' dependency is included in the plugin."); + throw new RuntimeException("SQLite driver not found", e); + } + + try { + databaseType = "sqlite"; + sqliteConnection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db"); + sqliteConnection.setAutoCommit(true); + logger.info("SQLite database initialized successfully."); + } catch (SQLException e) { + logger.severe("Failed to initialize SQLite database: " + e.getMessage()); + throw new RuntimeException("SQLite initialization failed", e); + } + } + + private void initializeMySQL() { + try { + // Use the dependency class loader to load MySQL driver + Class.forName("com.mysql.cj.jdbc.Driver", true, dependencyClassLoader); + } catch (ClassNotFoundException e) { + logger.severe("MySQL JDBC driver not found. Ensure 'mysql-connector-java' dependency is included in the plugin."); + logger.warning("Falling back to SQLite due to missing MySQL driver."); + initializeSQLite(); + return; + } + + try { + FileConfiguration config = plugin.getConfig(); + String host = config.getString("database.mysql.host", "localhost"); + int port = config.getInt("database.mysql.port", 3306); + String database = config.getString("database.mysql.database", "ollamachat"); + mysqlUsername = config.getString("database.mysql.username", "root"); + mysqlPassword = config.getString("database.mysql.password", ""); + mysqlUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true", host, port, database); + + // Test connection + try (Connection conn = DriverManager.getConnection(mysqlUrl, mysqlUsername, mysqlPassword)) { + logger.info("MySQL database initialized successfully."); + } + } catch (SQLException e) { + logger.severe("Failed to initialize MySQL database: " + e.getMessage()); + logger.warning("Falling back to SQLite due to MySQL initialization failure."); + initializeSQLite(); + } + } + + private Connection getConnection() throws SQLException { + if (databaseType.equals("sqlite")) { + if (sqliteConnection == null || sqliteConnection.isClosed()) { + sqliteConnection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db"); + sqliteConnection.setAutoCommit(true); + } + return sqliteConnection; + } else { + return DriverManager.getConnection(mysqlUrl, mysqlUsername, mysqlPassword); } } private void createTables() throws SQLException { - try (Statement stmt = connection.createStatement()) { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { stmt.execute("CREATE TABLE IF NOT EXISTS players (" + "uuid TEXT PRIMARY KEY," + "username TEXT NOT NULL)"); - stmt.execute("CREATE TABLE IF NOT EXISTS chat_history (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT," + + stmt.execute("CREATE TABLE IF NOT EXISTS conversations (" + + "conversation_id TEXT NOT NULL," + "player_uuid TEXT NOT NULL," + "ai_model TEXT NOT NULL," + + "conversation_name TEXT NOT NULL," + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," + + "PRIMARY KEY (conversation_id, player_uuid, ai_model)," + + "FOREIGN KEY (player_uuid) REFERENCES players(uuid))"); + + stmt.execute("CREATE TABLE IF NOT EXISTS chat_history (" + + "id INTEGER PRIMARY KEY " + (databaseType.equals("sqlite") ? "AUTOINCREMENT" : "AUTO_INCREMENT") + "," + + "player_uuid TEXT NOT NULL," + + "ai_model TEXT NOT NULL," + + "conversation_id TEXT," + "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," + "prompt TEXT NOT NULL," + "response TEXT NOT NULL," + - "FOREIGN KEY (player_uuid) REFERENCES players(uuid))"); + "FOREIGN KEY (player_uuid) REFERENCES players(uuid)," + + "FOREIGN KEY (conversation_id, player_uuid, ai_model) REFERENCES conversations(conversation_id, player_uuid, ai_model))"); } } public void savePlayerInfo(UUID uuid, String username) { - String sql = "INSERT OR REPLACE INTO players(uuid, username) VALUES(?, ?)"; - try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + String sql; + if (databaseType.equals("sqlite")) { + sql = "INSERT OR REPLACE INTO players (uuid, username) VALUES (?, ?)"; + } else { + sql = "INSERT INTO players (uuid, username) VALUES (?, ?) ON DUPLICATE KEY UPDATE username = ?"; + } + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, uuid.toString()); pstmt.setString(2, username); + if (databaseType.equals("mysql")) { + pstmt.setString(3, username); // For ON DUPLICATE KEY UPDATE + } pstmt.executeUpdate(); } catch (SQLException e) { + logger.severe("Failed to save player info: " + e.getMessage()); e.printStackTrace(); } } - public void saveChatHistory(UUID playerUuid, String aiModel, String prompt, String response) { - String sql = "INSERT INTO chat_history(player_uuid, ai_model, prompt, response) VALUES(?, ?, ?, ?)"; - try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + public String createConversation(UUID playerUuid, String aiModel, String convName) { + String convId = UUID.randomUUID().toString(); + String sql = "INSERT INTO conversations (conversation_id, player_uuid, ai_model, conversation_name) VALUES (?, ?, ?, ?)"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, convId); + pstmt.setString(2, playerUuid.toString()); + pstmt.setString(3, aiModel); + pstmt.setString(4, convName); + pstmt.executeUpdate(); + return convId; + } catch (SQLException e) { + logger.severe("Failed to create conversation: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + public boolean conversationExistsByName(UUID playerUuid, String aiModel, String convName) { + String sql = "SELECT 1 FROM conversations WHERE conversation_name = ? AND player_uuid = ? AND ai_model = ?"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, convName); + pstmt.setString(2, playerUuid.toString()); + pstmt.setString(3, aiModel); + try (ResultSet rs = pstmt.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + logger.severe("Failed to check conversation existence: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public String getConversationId(UUID playerUuid, String aiModel, String convName) { + String sql = "SELECT conversation_id FROM conversations WHERE conversation_name = ? AND player_uuid = ? AND ai_model = ?"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, convName); + pstmt.setString(2, playerUuid.toString()); + pstmt.setString(3, aiModel); + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + return rs.getString("conversation_id"); + } + } + } catch (SQLException e) { + logger.severe("Failed to get conversation ID: " + e.getMessage()); + e.printStackTrace(); + } + return null; + } + + public boolean conversationExists(UUID playerUuid, String aiModel, String convId) { + String sql = "SELECT 1 FROM conversations WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, convId); + pstmt.setString(2, playerUuid.toString()); + pstmt.setString(3, aiModel); + try (ResultSet rs = pstmt.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + logger.severe("Failed to check conversation existence: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public boolean deleteConversation(UUID playerUuid, String aiModel, String convId) { + String sqlDeleteHistory = "DELETE FROM chat_history WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?"; + String sqlDeleteConv = "DELETE FROM conversations WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?"; + try (Connection conn = getConnection(); + PreparedStatement pstmtHistory = conn.prepareStatement(sqlDeleteHistory); + PreparedStatement pstmtConv = conn.prepareStatement(sqlDeleteConv)) { + pstmtHistory.setString(1, convId); + pstmtHistory.setString(2, playerUuid.toString()); + pstmtHistory.setString(3, aiModel); + pstmtConv.setString(1, convId); + pstmtConv.setString(2, playerUuid.toString()); + pstmtConv.setString(3, aiModel); + int rowsAffected = pstmtHistory.executeUpdate() + pstmtConv.executeUpdate(); + return rowsAffected > 0; + } catch (SQLException e) { + logger.severe("Failed to delete conversation: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public Map listConversations(UUID playerUuid, String aiModel) { + Map conversations = new HashMap<>(); + String sql = "SELECT conversation_id, conversation_name FROM conversations WHERE player_uuid = ? AND ai_model = ?"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, playerUuid.toString()); pstmt.setString(2, aiModel); - pstmt.setString(3, prompt); - pstmt.setString(4, response); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + conversations.put(rs.getString("conversation_id"), rs.getString("conversation_name")); + } + } + } catch (SQLException e) { + logger.severe("Failed to list conversations: " + e.getMessage()); + e.printStackTrace(); + } + return conversations; + } + + public void saveChatHistory(UUID playerUuid, String aiModel, String conversationId, String prompt, String response) { + String sql = "INSERT INTO chat_history (player_uuid, ai_model, conversation_id, prompt, response) VALUES (?, ?, ?, ?, ?)"; + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, playerUuid.toString()); + pstmt.setString(2, aiModel); + if (conversationId != null) { + pstmt.setString(3, conversationId); + } else { + pstmt.setNull(3, Types.VARCHAR); + } + pstmt.setString(4, prompt); + pstmt.setString(5, response); pstmt.executeUpdate(); } catch (SQLException e) { + logger.severe("Failed to save chat history: " + e.getMessage()); e.printStackTrace(); } } - public String getChatHistory(UUID playerUuid, String aiModel, int maxHistory) { + public String getChatHistory(UUID playerUuid, String aiModel, String conversationId, int maxHistory) { StringBuilder history = new StringBuilder(); String sql = "SELECT prompt, response FROM chat_history " + "WHERE player_uuid = ? AND ai_model = ? " + + (conversationId != null ? "AND conversation_id = ? " : "AND conversation_id IS NULL ") + "ORDER BY timestamp DESC LIMIT ?"; - try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, playerUuid.toString()); pstmt.setString(2, aiModel); - pstmt.setInt(3, maxHistory); - ResultSet rs = pstmt.executeQuery(); - while (rs.next()) { - history.insert(0, "User: " + rs.getString("prompt") + "\n"); - history.insert(0, "AI: " + rs.getString("response") + "\n"); + int index = 3; + if (conversationId != null) { + pstmt.setString(index++, conversationId); + } + pstmt.setInt(index, maxHistory); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + history.insert(0, "User: " + rs.getString("prompt") + "\n"); + history.insert(0, "AI: " + rs.getString("response") + "\n"); + } } } catch (SQLException e) { + logger.severe("Failed to get chat history: " + e.getMessage()); e.printStackTrace(); } return history.toString(); @@ -83,11 +310,20 @@ public class DatabaseManager { public void close() { try { - if (connection != null) { - connection.close(); + if (databaseType.equals("sqlite") && sqliteConnection != null && !sqliteConnection.isClosed()) { + sqliteConnection.close(); } + // No need to close MySQL connections explicitly as they are managed per query } catch (SQLException e) { + logger.severe("Failed to close database: " + e.getMessage()); e.printStackTrace(); } } } + + + + + + + diff --git a/src/main/java/com/ollamachat/DependencyLoader.java b/src/main/java/com/ollamachat/DependencyLoader.java new file mode 100644 index 0000000..0459840 --- /dev/null +++ b/src/main/java/com/ollamachat/DependencyLoader.java @@ -0,0 +1,114 @@ +package com.ollamachat; + +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +public class DependencyLoader { + private final JavaPlugin plugin; + private final Logger logger; + private final File libDir; + private URLClassLoader dependencyClassLoader; + + public DependencyLoader(JavaPlugin plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + this.libDir = new File(plugin.getDataFolder(), "libs"); + } + + public boolean loadDependencies() { + try { + // Create libs directory if it doesn't exist + if (!libDir.exists()) { + libDir.mkdirs(); + } + + // Define dependencies to download + List dependencies = new ArrayList<>(); + dependencies.add(new Dependency("com.mysql", "mysql-connector-j", "8.0.33")); + dependencies.add(new Dependency("org.xerial", "sqlite-jdbc", "3.46.0.0")); + + // Download each dependency + List jarUrls = new ArrayList<>(); + for (Dependency dep : dependencies) { + File jarFile = downloadDependency(dep); + if (jarFile != null) { + jarUrls.add(jarFile.toURI().toURL()); + logger.info("Found dependency: " + jarFile.getName()); + } else { + logger.warning("Failed to download dependency: " + dep.artifactId + "-" + dep.version); + } + } + + if (jarUrls.isEmpty()) { + logger.warning("No dependencies were successfully downloaded."); + return false; + } + + // Create a new classloader with the dependencies + dependencyClassLoader = new URLClassLoader( + jarUrls.toArray(new URL[0]), + plugin.getClass().getClassLoader() + ); + + // Set this classloader as the thread context classloader + Thread.currentThread().setContextClassLoader(dependencyClassLoader); + logger.info("Successfully loaded " + jarUrls.size() + " dependencies"); + + return true; + } catch (Exception e) { + logger.severe("Failed to load dependencies: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public ClassLoader getDependencyClassLoader() { + return dependencyClassLoader; + } + + private File downloadDependency(Dependency dep) { + String fileName = dep.artifactId + "-" + dep.version + ".jar"; + File file = new File(libDir, fileName); + + if (file.exists()) { + return file; + } + + String mavenPath = String.format("%s/%s/%s/%s-%s.jar", + dep.groupId.replace(".", "/"), dep.artifactId, dep.version, dep.artifactId, dep.version); + String url = "https://repo1.maven.org/maven2/" + mavenPath; + + try (InputStream in = new URL(url).openStream()) { + Files.copy(in, file.toPath()); + logger.info("Downloaded dependency: " + fileName); + return file; + } catch (IOException e) { + logger.severe("Failed to download dependency " + fileName + ": " + e.getMessage()); + return null; + } + } + + private static class Dependency { + String groupId; + String artifactId; + String version; + + Dependency(String groupId, String artifactId, String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + } +} + + + + diff --git a/src/main/java/com/ollamachat/Ollamachat.java b/src/main/java/com/ollamachat/Ollamachat.java deleted file mode 100644 index c3a6f2c..0000000 --- a/src/main/java/com/ollamachat/Ollamachat.java +++ /dev/null @@ -1,352 +0,0 @@ -package com.ollamachat; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import net.kyori.adventure.text.Component; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public class Ollamachat extends JavaPlugin implements Listener { - - private AIService aiService; - private Gson gson; - private String ollamaApiUrl; - private String ollamaModel; - private String triggerPrefix; - private int maxResponseLength; - private Map otherAIConfigs; - private boolean ollamaEnabled; - private Map otherAIEnabled; - - private FileConfiguration langConfig; - private DatabaseManager databaseManager; - private ChatHistoryManager chatHistoryManager; - private int maxHistory; - - @Override - public void onEnable() { - saveDefaultConfig(); - reloadConfigValues(); - String language = getConfig().getString("language", "en"); - loadLanguageFile(language); - - updateCommandUsages(); - - databaseManager = new DatabaseManager(); - maxHistory = getConfig().getInt("max-history", 5); - chatHistoryManager = new ChatHistoryManager(databaseManager, maxHistory); - - aiService = new AIService(); - gson = new Gson(); - - getServer().getPluginManager().registerEvents(this, this); - getCommand("ollamachat").setExecutor(this); - getCommand("aichat").setExecutor(this); - } - - private void updateCommandUsages() { - String usageOllamachat = getMessage("usage-ollamachat", null); - String usageAichat = getMessage("usage-aichat", null); - - getCommand("ollamachat").setUsage(usageOllamachat); - getCommand("aichat").setUsage(usageAichat); - } - - @Override - public void onDisable() { - databaseManager.close(); - } - - private void updateConfig() { - FileConfiguration config = getConfig(); - - if (!config.contains("ollama-enabled")) { - config.set("ollama-enabled", true); - } - if (!config.contains("language")) { - config.set("language", "en"); - } - if (!config.contains("other-ai-configs")) { - config.createSection("other-ai-configs"); - } - if (!config.contains("max-history")) { - config.set("max-history", 5); - } - - saveConfig(); - } - - private void reloadConfigValues() { - File configFile = new File(getDataFolder(), "config.yml"); - if (!configFile.exists()) { - saveDefaultConfig(); - } else { - try { - YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); - if (!config.contains("ollama-api-url") || !config.contains("model")) { - getLogger().warning(getMessage("config-invalid", null)); - configFile.delete(); - saveDefaultConfig(); - } - } catch (Exception e) { - getLogger().severe(getMessage("config-load-failed", Map.of("error", e.getMessage()))); - configFile.delete(); - saveDefaultConfig(); - } - } - - reloadConfig(); - updateConfig(); - - FileConfiguration config = getConfig(); - ollamaApiUrl = config.getString("ollama-api-url", "http://localhost:11434/api/generate"); - ollamaModel = config.getString("model", "llama3"); - triggerPrefix = config.getString("trigger-prefix", "@bot "); - maxResponseLength = config.getInt("max-response-length", 500); - ollamaEnabled = config.getBoolean("ollama-enabled", true); - maxHistory = config.getInt("max-history", 5); - - otherAIConfigs = new HashMap<>(); - otherAIEnabled = new HashMap<>(); - if (config.contains("other-ai-configs")) { - for (String aiName : config.getConfigurationSection("other-ai-configs").getKeys(false)) { - String apiUrl = config.getString("other-ai-configs." + aiName + ".api-url"); - String apiKey = config.getString("other-ai-configs." + aiName + ".api-key"); - String model = config.getString("other-ai-configs." + aiName + ".model"); - boolean enabled = config.getBoolean("other-ai-configs." + aiName + ".enabled", true); - otherAIConfigs.put(aiName, new AIConfig(apiUrl, apiKey, model)); - otherAIEnabled.put(aiName, enabled); - } - } - } - - private void loadLanguageFile(String language) { - File langFolder = new File(getDataFolder(), "lang"); - if (!langFolder.exists()) { - langFolder.mkdirs(); - } - - File langFile = new File(langFolder, language + ".lang"); - if (!langFile.exists()) { - saveResource("lang/" + language + ".lang", false); - } - - try { - langConfig = YamlConfiguration.loadConfiguration(langFile); - } catch (Exception e) { - getLogger().severe("Failed to load language file: " + langFile.getName()); - e.printStackTrace(); - } - } - - private String getMessage(String key, Map placeholders) { - String message = langConfig.getString(key, "§cMissing language key: " + key); - if (placeholders != null) { - for (Map.Entry entry : placeholders.entrySet()) { - message = message.replace("{" + entry.getKey() + "}", entry.getValue()); - } - } - return message; - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - chatHistoryManager.savePlayerInfo(event.getPlayer()); - } - - @EventHandler - public void onPlayerChat(AsyncPlayerChatEvent event) { - if (!ollamaEnabled) return; - - String message = event.getMessage(); - Player player = event.getPlayer(); - - if (message.startsWith(triggerPrefix)) { - /*event.setCancelled(true);*/ - String prompt = message.substring(triggerPrefix.length()).trim(); - - if (!prompt.isEmpty()) { - processOllamaQueryAsync(player, prompt); - } - } - } - - private void processOllamaQueryAsync(Player player, String prompt) { - CompletableFuture.runAsync(() -> { - try { - String history = chatHistoryManager.getChatHistory(player.getUniqueId(), "ollama"); - - String context = history + "User: " + prompt; - - String responseBody = aiService.sendRequest(ollamaApiUrl, null, ollamaModel, context).join(); - OllamaResponse ollamaResponse = gson.fromJson(responseBody, OllamaResponse.class); - - chatHistoryManager.saveChatHistory(player.getUniqueId(), "ollama", prompt, ollamaResponse.response); - - sendFormattedResponse(player, ollamaResponse.response); - } catch (Exception e) { - getLogger().severe("Error processing Ollama request: " + e.getMessage()); - sendErrorMessage(player, getMessage("error-prefix", null) + "Failed to get response from Ollama"); - } - }); - } - - private void processOtherAIQueryAsync(Player player, String aiName, String prompt) { - if (!otherAIEnabled.getOrDefault(aiName, false)) { - sendErrorMessage(player, getMessage("error-prefix", null) + getMessage("toggle-disabled", Map.of("ai-name", aiName))); - return; - } - - CompletableFuture.runAsync(() -> { - try { - String history = chatHistoryManager.getChatHistory(player.getUniqueId(), aiName); - - String context = history + "User: " + prompt; - - AIConfig aiConfig = otherAIConfigs.get(aiName); - String responseBody = aiService.sendRequest( - aiConfig.getApiUrl(), - aiConfig.getApiKey(), - aiConfig.getModel(), - context - ).join(); - - String response = parseAIResponse(aiName, responseBody); - - chatHistoryManager.saveChatHistory( - player.getUniqueId(), - aiName, - prompt, - response - ); - - sendFormattedResponse(player, response); - } catch (Exception e) { - getLogger().severe("Error processing " + aiName + " request: " + e.getMessage()); - sendErrorMessage(player, getMessage("error-prefix", null) + "Failed to get response from " + aiName); - } - }); - } - - private String parseAIResponse(String aiName, String responseBody) { - switch (aiName.toLowerCase()) { - case "openai": - JsonObject json = gson.fromJson(responseBody, JsonObject.class); - return json.getAsJsonArray("choices") - .get(0).getAsJsonObject() - .get("message").getAsJsonObject() - .get("content").getAsString(); - default: - return gson.fromJson(responseBody, OllamaResponse.class).response; - } - } - - private void sendFormattedResponse(Player player, String response) { - - response = response.replaceAll("User:", "§7User:").replaceAll("AI:", ""); - - if (response.trim().isEmpty()) { - Bukkit.broadcast(Component.text(getMessage("response-prefix", null) + "§cNastala chyba, zkuste se zeptat znovu.")); - return; - } - - if (response.length() > maxResponseLength) { - response = response.substring(0, maxResponseLength) + "..."; - } - - Bukkit.broadcast(Component.text(getMessage("response-prefix", null) + response)); - /*player.sendMessage(getMessage("response-prefix", null) + response);*/ - } - - private void sendErrorMessage(Player player, String errorMessage) { - player.sendMessage(getMessage("error-prefix", null) + errorMessage); - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (command.getName().equalsIgnoreCase("ollamachat")) { - if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { - reloadConfigValues(); - loadLanguageFile(getConfig().getString("language", "en")); - sender.sendMessage(getMessage("reload-success", null)); - return true; - } else if (args.length > 1 && args[0].equalsIgnoreCase("toggle")) { - String aiName = args[1]; - if (aiName.equalsIgnoreCase("ollama")) { - ollamaEnabled = !ollamaEnabled; - sender.sendMessage(getMessage(ollamaEnabled ? "ollama-enabled" : "ollama-disabled", null)); - } else if (otherAIConfigs.containsKey(aiName)) { - boolean newState = !otherAIEnabled.getOrDefault(aiName, false); - otherAIEnabled.put(aiName, newState); - sender.sendMessage(getMessage(newState ? "toggle-enabled" : "toggle-disabled", Map.of("ai-name", aiName))); - } else { - sender.sendMessage(getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", otherAIConfigs.keySet())))); - } - return true; - } - } else if (command.getName().equalsIgnoreCase("aichat")) { - if (args.length < 2) { - sender.sendMessage(getMessage("usage-aichat", null)); - return true; - } - - String aiName = args[0]; - String prompt = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length)); - - if (otherAIConfigs.containsKey(aiName)) { - if (sender instanceof Player) { - processOtherAIQueryAsync((Player) sender, aiName, prompt); - } else { - sender.sendMessage(getMessage("player-only", null)); - } - } else { - sender.sendMessage(getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", otherAIConfigs.keySet())))); - } - return true; - } - return false; - } - - private static class AIConfig { - private final String apiUrl; - private final String apiKey; - private final String model; - - public AIConfig(String apiUrl, String apiKey, String model) { - this.apiUrl = apiUrl; - this.apiKey = apiKey; - this.model = model; - } - - public String getApiUrl() { - return apiUrl; - } - - public String getApiKey() { - return apiKey; - } - - public String getModel() { - return model; - } - } - - private static class OllamaResponse { - public String response; - } -} \ No newline at end of file diff --git a/src/main/java/com/ollamachat/ProgressManager.java b/src/main/java/com/ollamachat/ProgressManager.java new file mode 100644 index 0000000..07d8a6c --- /dev/null +++ b/src/main/java/com/ollamachat/ProgressManager.java @@ -0,0 +1,104 @@ +package com.ollamachat; + +import com.ollamachat.core.Ollamachat; +import com.ollamachat.core.ConfigManager; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ProgressManager { + private final Ollamachat plugin; + private final ConfigManager configManager; // Add ConfigManager reference + private final Map bossBars = new HashMap<>(); + private final Map tasks = new HashMap<>(); + + public ProgressManager(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); // Initialize ConfigManager + } + + public void startProgress(Player player, String title, BarColor color, BarStyle style) { + UUID uuid = player.getUniqueId(); + cleanup(player); + + if (plugin.getConfig().getString("progress-display.type", "bossbar").equalsIgnoreCase("bossbar")) { + handleBossBarProgress(player, uuid, title, color, style); + } else { + handleActionBarProgress(player, uuid); + } + } + + private void handleBossBarProgress(Player player, UUID uuid, String title, BarColor color, BarStyle style) { + BossBar bossBar = Bukkit.createBossBar( + configManager.getMessage("generating-status", Map.of("progress", "0")), // Use configManager + color, + style + ); + bossBar.addPlayer(player); + bossBar.setProgress(0.0); + bossBar.setVisible(true); + bossBars.put(uuid, bossBar); + + long interval = 20L * plugin.getConfig().getInt("progress-display.update-interval", 1); + tasks.put(uuid, Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (!player.isOnline()) { + cleanup(player); + } + }, 0, interval)); + } + + private void handleActionBarProgress(Player player, UUID uuid) { + long interval = 20L * plugin.getConfig().getInt("progress-display.update-interval", 1); + tasks.put(uuid, Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (!player.isOnline()) { + cleanup(player); + return; + } + player.sendActionBar(configManager.getMessage("generating-status", Map.of("progress", "0"))); // Use configManager + }, 0, interval)); + } + + public void complete(Player player) { + UUID uuid = player.getUniqueId(); + if (bossBars.containsKey(uuid)) { + BossBar bossBar = bossBars.get(uuid); + bossBar.setProgress(1.0); + bossBar.setTitle(configManager.getMessage("complete-status", null)); // Use configManager + bossBar.setColor(BarColor.GREEN); + Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L); + } else { + Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L); + } + } + + public void error(Player player) { + UUID uuid = player.getUniqueId(); + if (bossBars.containsKey(uuid)) { + BossBar bossBar = bossBars.get(uuid); + bossBar.setTitle(configManager.getMessage("error-status", null)); // Use configManager + bossBar.setColor(BarColor.RED); + Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L); + } + } + + public void cleanup(Player player) { + UUID uuid = player.getUniqueId(); + if (tasks.containsKey(uuid)) { + tasks.get(uuid).cancel(); + tasks.remove(uuid); + } + if (bossBars.containsKey(uuid)) { + bossBars.get(uuid).removeAll(); + bossBars.remove(uuid); + } + } +} + + diff --git a/src/main/java/com/ollamachat/chat/ChatTriggerHandler.java b/src/main/java/com/ollamachat/chat/ChatTriggerHandler.java new file mode 100644 index 0000000..c9937b2 --- /dev/null +++ b/src/main/java/com/ollamachat/chat/ChatTriggerHandler.java @@ -0,0 +1,164 @@ +package com.ollamachat.chat; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.ollamachat.AIService; +import com.ollamachat.core.ConfigManager; +import com.ollamachat.core.Ollamachat; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ChatTriggerHandler implements Listener { + private final Ollamachat plugin; + private final ConfigManager configManager; + private final AIService aiService; + private final SuggestedResponseHandler suggestedResponseHandler; + private final Gson gson; + + public ChatTriggerHandler(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); + this.aiService = new AIService(); + this.suggestedResponseHandler = new SuggestedResponseHandler(plugin); + this.gson = new Gson(); + } + + @EventHandler + public void onPlayerChat(AsyncPlayerChatEvent event) { + if (!configManager.isOllamaEnabled()) return; + + String message = event.getMessage(); + Player player = event.getPlayer(); + + for (String prefix : configManager.getTriggerPrefixes()) { + if (message.startsWith(prefix)) { + //event.setCancelled(true); + String prompt = message.substring(prefix.length()).trim(); + if (!prompt.isEmpty()) { + processAIQuery(player, "ollama", prompt); + } + break; + } + } + } + + public void processAIQuery(Player player, String aiName, String prompt) { + if (!aiName.equalsIgnoreCase("ollama") && !configManager.getOtherAIEnabled().getOrDefault(aiName, false)) { + sendErrorMessage(player, configManager.getMessage("error-prefix", null) + configManager.getMessage("toggle-disabled", Map.of("ai-name", aiName))); + return; + } + + if (plugin.getConfig().getBoolean("progress-display.enabled", true)) { + BarColor color = BarColor.valueOf(plugin.getConfig().getString("progress-display.color", "BLUE")); + BarStyle style = BarStyle.valueOf(plugin.getConfig().getString("progress-display.style", "SOLID")); + plugin.getProgressManager().startProgress(player, configManager.getMessage("generating-status", null), color, style); + } + + CompletableFuture.runAsync(() -> { + try { + UUID playerUuid = player.getUniqueId(); + // Save player info to ensure uuid exists in players table + plugin.getChatHistoryManager().savePlayerInfo(player); + + String conversationName = configManager.getSelectedConversations() + .computeIfAbsent(playerUuid, k -> new HashMap<>()) + .getOrDefault(aiName, null); + String conversationId = conversationName != null + ? plugin.getChatHistoryManager().getConversationId(playerUuid, aiName, conversationName) + : null; + String history = plugin.getChatHistoryManager().getChatHistory(playerUuid, aiName, conversationId); + String selectedPrompt = configManager.getPrompts().getOrDefault(configManager.getDefaultPrompt(), ""); + String context = history + (selectedPrompt.isEmpty() ? "" : selectedPrompt + "\n") + "User: " + prompt; + + String apiUrl, apiKey, model; + boolean isMessagesFormat; + if (aiName.equalsIgnoreCase("ollama")) { + apiUrl = configManager.getOllamaApiUrl(); + apiKey = null; + model = configManager.getOllamaModel(); + isMessagesFormat = false; + } else { + ConfigManager.AIConfig aiConfig = configManager.getOtherAIConfigs().get(aiName); + apiUrl = aiConfig.getApiUrl(); + apiKey = aiConfig.getApiKey(); + model = aiConfig.getModel(); + isMessagesFormat = aiConfig.isMessagesFormat(); + } + + String finalResponse; + if (configManager.isStreamingEnabled()) { + StringBuilder fullResponse = new StringBuilder(); + AtomicBoolean isFirstMessage = new AtomicBoolean(true); + aiService.sendStreamingRequest(apiUrl, apiKey, model, context, partialResponse -> { + if (player.isOnline()) { + String formattedPartial = partialResponse.length() > configManager.getMaxResponseLength() + ? partialResponse.substring(0, configManager.getMaxResponseLength()) + "..." + : partialResponse; + String message = isFirstMessage.get() + ? configManager.getMessage("response-prefix", null) + formattedPartial + : formattedPartial; + //player.sendMessage(message); + Bukkit.broadcastMessage(message); + isFirstMessage.set(false); + fullResponse.append(partialResponse); + } + }, isMessagesFormat).join(); + finalResponse = fullResponse.toString(); + } else { + String responseBody = aiService.sendRequest(apiUrl, apiKey, model, context, isMessagesFormat).join(); + if (isMessagesFormat) { + JsonObject json = gson.fromJson(responseBody, JsonObject.class); + finalResponse = json.getAsJsonArray("choices") + .get(0).getAsJsonObject() + .get("message").getAsJsonObject() + .get("content").getAsString(); + } else { + JsonObject json = gson.fromJson(responseBody, JsonObject.class); + finalResponse = json.get("response").getAsString(); + } + if (player.isOnline()) { + sendFormattedResponse(player, finalResponse); + } + } + + if (!finalResponse.isEmpty()) { + plugin.getChatHistoryManager().saveChatHistory(playerUuid, aiName, conversationId, prompt, finalResponse); + suggestedResponseHandler.sendSuggestedResponses(player, aiName, prompt, finalResponse); + } + plugin.getProgressManager().complete(player); + } catch (Exception e) { + plugin.getLogger().severe("Error processing " + aiName + " request: " + e.getMessage()); + if (player.isOnline()) { + sendErrorMessage(player, configManager.getMessage("error-prefix", null) + "Failed to get response from " + aiName); + } + plugin.getProgressManager().error(player); + } + }); + } + + private void sendFormattedResponse(Player player, String response) { + if (response.length() > configManager.getMaxResponseLength()) { + response = response.substring(0, configManager.getMaxResponseLength()) + "..."; + } + //player.sendMessage(configManager.getMessage("response-prefix", null) + response); + Bukkit.broadcastMessage(configManager.getMessage("response-prefix", null) + response); + } + + private void sendErrorMessage(Player player, String errorMessage) { + player.sendMessage(errorMessage); + } +} + + + diff --git a/src/main/java/com/ollamachat/chat/SuggestedResponseHandler.java b/src/main/java/com/ollamachat/chat/SuggestedResponseHandler.java new file mode 100644 index 0000000..802da9b --- /dev/null +++ b/src/main/java/com/ollamachat/chat/SuggestedResponseHandler.java @@ -0,0 +1,181 @@ +package com.ollamachat.chat; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.ollamachat.AIService; +import com.ollamachat.core.ConfigManager; +import com.ollamachat.core.Ollamachat; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.*; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class SuggestedResponseHandler { + private final Ollamachat plugin; + private final ConfigManager configManager; + private final AIService aiService; + private final Gson gson; + private final Map lastSuggestionTimes; + + public SuggestedResponseHandler(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); + this.aiService = new AIService(); + this.gson = new Gson(); + this.lastSuggestionTimes = new HashMap<>(); + } + + public boolean isSuggestionsEnabledForPlayer(Player player) { + Map playerSuggestionToggles = plugin.getPlayerSuggestionToggles(); + return playerSuggestionToggles.computeIfAbsent(player.getUniqueId(), k -> true); + } + + public void toggleSuggestionsForPlayer(Player player, boolean enabled) { + plugin.getPlayerSuggestionToggles().put(player.getUniqueId(), enabled); + } + + public boolean canGenerateSuggestions(Player player) { + int cooldown = configManager.getSuggestedResponseCooldown(); + if (cooldown <= 0) return true; + long currentTime = System.currentTimeMillis(); + Long lastTime = lastSuggestionTimes.get(player.getUniqueId()); + if (lastTime == null || (currentTime - lastTime) / 1000 >= cooldown) { + lastSuggestionTimes.put(player.getUniqueId(), currentTime); + return true; + } + return false; + } + + public void sendSuggestedResponses(Player player, String originalAIName, String originalPrompt, String originalResponse) { + if (!configManager.isSuggestedResponsesEnabled() || !isSuggestionsEnabledForPlayer(player)) return; + if (!canGenerateSuggestions(player)) { + int cooldown = configManager.getSuggestedResponseCooldown(); + long lastTime = lastSuggestionTimes.get(player.getUniqueId()); + long secondsRemaining = cooldown - (System.currentTimeMillis() - lastTime) / 1000; + sendMessage(player, ChatColor.RED + configManager.getMessage("suggests-rate-limit", + Map.of("seconds", String.valueOf(secondsRemaining)))); + return; + } + + List suggestedResponses = new ArrayList<>(); + if (configManager.isSuggestedResponsePresetsEnabled()) { + suggestedResponses.addAll(configManager.getSuggestedResponsePresets()); + } + + List suggestedModels = configManager.getSuggestedResponseModels(); + if (!suggestedModels.isEmpty()) { + CompletableFuture.runAsync(() -> { + for (String model : suggestedModels) { + if (model.equalsIgnoreCase(originalAIName)) continue; + if (!configManager.getSuggestedResponseModelToggles().getOrDefault(model, true)) continue; + try { + String apiUrl; + String apiKey; + boolean isMessagesFormat; + ConfigManager.AIConfig aiConfig = configManager.getOtherAIConfigs().get(model); + if (aiConfig != null && configManager.getOtherAIEnabled().getOrDefault(model, false)) { + apiUrl = aiConfig.getApiUrl(); + apiKey = aiConfig.getApiKey(); + isMessagesFormat = aiConfig.isMessagesFormat(); + } else { + apiUrl = configManager.getOllamaApiUrl(); + apiKey = null; + isMessagesFormat = false; + } + String promptTemplate = configManager.getSuggestedResponsePrompt(); + String context = promptTemplate + .replace("{prompt}", originalPrompt) + .replace("{response}", originalResponse) + .replace("{count}", String.valueOf(configManager.getSuggestedResponseCount())); + String responseBody = aiService.sendRequest(apiUrl, apiKey, model, context, isMessagesFormat).join(); + String suggestedText; + if (isMessagesFormat) { + JsonObject json = gson.fromJson(responseBody, JsonObject.class); + suggestedText = json.getAsJsonArray("choices") + .get(0).getAsJsonObject() + .get("message").getAsJsonObject() + .get("content").getAsString(); + } else { + JsonObject json = gson.fromJson(responseBody, JsonObject.class); + suggestedText = json.get("response").getAsString(); + } + String[] suggestions = suggestedText.split("\n"); + for (String suggestion : suggestions) { + String cleanedSuggestion = suggestion.replaceAll("^\\d+\\.\\s*", "").trim(); + if (!cleanedSuggestion.isEmpty()) { + suggestedResponses.add("[" + model + "] " + cleanedSuggestion); + } + } + } catch (Exception e) { + plugin.getLogger().warning("Failed to get suggested response from " + model + ": " + e.getMessage()); + } + } + + if (player.isOnline() && !suggestedResponses.isEmpty()) { + sendMessage(player, ChatColor.GREEN + configManager.getMessage("suggested-responses-header", null)); + for (String response : suggestedResponses) { + if (response.length() > configManager.getMaxResponseLength()) { + response = response.substring(0, configManager.getMaxResponseLength()) + "..."; + sendMessage(player, ChatColor.YELLOW + configManager.getMessage("response-truncated", null)); + } + sendClickableMessage(player, + configManager.getMessage("suggested-response-prefix", null) + response, + "/aichat " + originalAIName + " " + response.replace("\"", "\\\""), + configManager.getMessage("suggested-response-hover", null)); + } + } + }); + } else if (player.isOnline() && !suggestedResponses.isEmpty()) { + sendMessage(player, ChatColor.GREEN + configManager.getMessage("suggested-responses-header", null)); + for (String response : suggestedResponses) { + if (response.length() > configManager.getMaxResponseLength()) { + response = response.substring(0, configManager.getMaxResponseLength()) + "..."; + sendMessage(player, ChatColor.YELLOW + configManager.getMessage("response-truncated", null)); + } + sendClickableMessage(player, + configManager.getMessage("suggested-response-prefix", null) + response, + "/aichat " + originalAIName + " " + response.replace("\"", "\\\""), + configManager.getMessage("suggested-response-hover", null)); + } + } + } + + private void sendMessage(Player player, String message) { + if (player.isOnline()) { + player.sendMessage(message); + } + } + + private void sendClickableMessage(Player player, String text, String command, String hoverText) { + TextComponent message = new TextComponent(text); + message.setColor(ChatColor.WHITE); + message.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)); + message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(hoverText).color(ChatColor.YELLOW).create())); + player.spigot().sendMessage(message); + } +} + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/ollamachat/command/AIChatCommand.java b/src/main/java/com/ollamachat/command/AIChatCommand.java new file mode 100644 index 0000000..1c3f8ca --- /dev/null +++ b/src/main/java/com/ollamachat/command/AIChatCommand.java @@ -0,0 +1,48 @@ +package com.ollamachat.command; + +import com.ollamachat.core.Ollamachat; +import com.ollamachat.core.ConfigManager; +import com.ollamachat.chat.ChatTriggerHandler; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.Map; + +public class AIChatCommand implements CommandExecutor { + private final Ollamachat plugin; + private final ConfigManager configManager; + private final ChatTriggerHandler chatTriggerHandler; + + public AIChatCommand(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); + this.chatTriggerHandler = new ChatTriggerHandler(plugin); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length < 2) { + sender.sendMessage(configManager.getMessage("usage-aichat", null)); + return true; + } + + String aiName = args[0]; + String prompt = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + + if (aiName.equalsIgnoreCase("ollama") || configManager.getOtherAIConfigs().containsKey(aiName)) { + if (sender instanceof Player) { + chatTriggerHandler.processAIQuery((Player) sender, aiName, prompt); + } else { + sender.sendMessage(configManager.getMessage("player-only", null)); + } + } else { + sender.sendMessage(configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet())))); + } + return true; + } +} + + diff --git a/src/main/java/com/ollamachat/command/OllamaChatCommand.java b/src/main/java/com/ollamachat/command/OllamaChatCommand.java new file mode 100644 index 0000000..d868496 --- /dev/null +++ b/src/main/java/com/ollamachat/command/OllamaChatCommand.java @@ -0,0 +1,265 @@ +package com.ollamachat.command; + +import com.ollamachat.core.Ollamachat; +import com.ollamachat.core.ConfigManager; +import com.ollamachat.chat.ChatTriggerHandler; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class OllamaChatCommand implements CommandExecutor { + private final Ollamachat plugin; + private final ConfigManager configManager; + private final ChatTriggerHandler chatTriggerHandler; + + public OllamaChatCommand(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); + this.chatTriggerHandler = new ChatTriggerHandler(plugin); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0) { + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null)); + return true; + } + + if (args[0].equalsIgnoreCase("reload")) { + if (!sender.hasPermission("ollamachat.reload")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + configManager.reloadConfigValues(); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("reload-success", null)); + return true; + } else if (args[0].equalsIgnoreCase("toggle") && args.length > 1) { + if (!sender.hasPermission("ollamachat.toggle")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String aiName = args[1]; + if (aiName.equalsIgnoreCase("ollama")) { + configManager.setOllamaEnabled(!configManager.isOllamaEnabled()); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage(configManager.isOllamaEnabled() ? "ollama-enabled" : "ollama-disabled", null)); + } else if (configManager.getOtherAIConfigs().containsKey(aiName)) { + boolean newState = !configManager.getOtherAIEnabled().getOrDefault(aiName, false); + configManager.getOtherAIEnabled().put(aiName, newState); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage(newState ? "toggle-enabled" : "toggle-disabled", Map.of("ai-name", aiName))); + } else { + sender.sendMessage(ChatColor.RED + configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet())))); + } + return true; + } else if (args[0].equalsIgnoreCase("prompt") && args.length > 1) { + String subCommand = args[1].toLowerCase(); + if (subCommand.equals("set") && args.length > 3) { + if (!sender.hasPermission("ollamachat.prompt.set")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String promptName = args[2]; + String promptContent = String.join(" ", Arrays.copyOfRange(args, 3, args.length)); + plugin.getConfig().set("prompts." + promptName, promptContent); + plugin.saveConfig(); + configManager.getPrompts().put(promptName, promptContent); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-set", Map.of("name", promptName))); + return true; + } else if (subCommand.equals("delete") && args.length == 3) { + if (!sender.hasPermission("ollamachat.prompt.delete")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String promptName = args[2]; + if (configManager.getPrompts().containsKey(promptName)) { + plugin.getConfig().set("prompts." + promptName, null); + if (configManager.getDefaultPrompt().equals(promptName)) { + plugin.getConfig().set("default-prompt", ""); + configManager.setDefaultPrompt(""); + } + plugin.saveConfig(); + configManager.getPrompts().remove(promptName); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-deleted", Map.of("name", promptName))); + } else { + sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-not-found", Map.of("name", promptName))); + } + return true; + } else if (subCommand.equals("list")) { + if (!sender.hasPermission("ollamachat.prompt.list")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + if (configManager.getPrompts().isEmpty()) { + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-list-empty", null)); + } else { + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-list", Map.of("prompts", String.join(", ", configManager.getPrompts().keySet())))); + } + if (!configManager.getDefaultPrompt().isEmpty() && configManager.getPrompts().containsKey(configManager.getDefaultPrompt())) { + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-default", Map.of("name", configManager.getDefaultPrompt()))); + } else if (!configManager.getDefaultPrompt().isEmpty()) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-default-invalid", Map.of("name", configManager.getDefaultPrompt()))); + } + return true; + } else if (subCommand.equals("select") && args.length == 3) { + if (!sender.hasPermission("ollamachat.prompt.select")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String promptName = args[2]; + if (configManager.getPrompts().containsKey(promptName)) { + plugin.getConfig().set("default-prompt", promptName); + plugin.saveConfig(); + configManager.setDefaultPrompt(promptName); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-selected", Map.of("name", promptName))); + } else { + sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-not-found", Map.of("name", promptName))); + } + return true; + } else if (subCommand.equals("clear")) { + if (!sender.hasPermission("ollamachat.prompt.select")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + plugin.getConfig().set("default-prompt", ""); + plugin.saveConfig(); + configManager.setDefaultPrompt(""); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-cleared", null)); + return true; + } else { + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("prompt-usage", null)); + return true; + } + } else if (args[0].equalsIgnoreCase("conversation") && args.length > 1) { + if (!(sender instanceof Player)) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("player-only", null)); + return true; + } + Player player = (Player) sender; + String subCommand = args[1].toLowerCase(); + String aiName = args.length > 2 ? args[2] : "ollama"; + if (!aiName.equals("ollama") && !configManager.getOtherAIConfigs().containsKey(aiName)) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet())))); + return true; + } + if (subCommand.equals("new") && args.length == 4) { + if (!sender.hasPermission("ollamachat.conversation.new")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String convName = args[3]; + String convId = plugin.getChatHistoryManager().createConversation(player.getUniqueId(), aiName, convName); + configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(aiName, convName); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-created", Map.of("name", convName, "ai-name", aiName))); + return true; + } else if (subCommand.equals("select") && args.length == 4) { + if (!sender.hasPermission("ollamachat.conversation.select")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String convName = args[3]; + if (plugin.getChatHistoryManager().conversationExistsByName(player.getUniqueId(), aiName, convName)) { + configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(aiName, convName); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-selected", Map.of("name", convName, "ai-name", aiName))); + } else { + sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-not-found", Map.of("name", convName))); + } + return true; + } else if (subCommand.equals("delete") && args.length == 4) { + if (!sender.hasPermission("ollamachat.conversation.delete")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String convName = args[3]; + String convId = plugin.getChatHistoryManager().getConversationId(player.getUniqueId(), aiName, convName); + if (convId != null && plugin.getChatHistoryManager().deleteConversation(player.getUniqueId(), aiName, convId)) { + Map convMap = configManager.getSelectedConversations().get(player.getUniqueId()); + if (convMap != null && convName.equals(convMap.get(aiName))) { + convMap.remove(aiName); + } + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-deleted", Map.of("name", convName, "ai-name", aiName))); + } else { + sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-not-found", Map.of("name", convName))); + } + return true; + } else if (subCommand.equals("list") && args.length == 3) { + if (!sender.hasPermission("ollamachat.conversation.list")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + Map conversations = plugin.getChatHistoryManager().listConversations(player.getUniqueId(), aiName); + if (conversations.isEmpty()) { + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-list-empty", Map.of("ai-name", aiName))); + } else { + String convList = String.join(", ", conversations.values()); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-list", Map.of("conversations", convList, "ai-name", aiName))); + } + String selectedConv = configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).get(aiName); + if (selectedConv != null && conversations.containsValue(selectedConv)) { + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-default", Map.of("name", selectedConv, "ai-name", aiName))); + } else if (selectedConv != null) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-default-invalid", Map.of("name", selectedConv, "ai-name", aiName))); + } + return true; + } else { + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("conversation-usage", null)); + return true; + } + } else if (args[0].equalsIgnoreCase("suggests") && args.length == 2) { + if (!(sender instanceof Player)) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("player-only", null)); + return true; + } + if (!sender.hasPermission("ollamachat.suggests.toggle")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + Player player = (Player) sender; + String subCommand = args[1].toLowerCase(); + if (subCommand.equals("on")) { + plugin.getSuggestedResponseHandler().toggleSuggestionsForPlayer(player, true); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("suggests-enabled", Map.of("player", player.getName()))); + return true; + } else if (subCommand.equals("off")) { + plugin.getSuggestedResponseHandler().toggleSuggestionsForPlayer(player, false); + sender.sendMessage(ChatColor.RED + configManager.getMessage("suggests-disabled", Map.of("player", player.getName()))); + return true; + } else { + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null)); + return true; + } + } else if (args[0].equalsIgnoreCase("suggests-presets") && args.length == 2) { + if (!sender.hasPermission("ollamachat.suggests-presets.toggle")) { + sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null)); + return true; + } + String subCommand = args[1].toLowerCase(); + if (subCommand.equals("on")) { + configManager.setSuggestedResponsePresetsEnabled(true); + sender.sendMessage(ChatColor.GREEN + configManager.getMessage("suggests-presets-enabled", null)); + return true; + } else if (subCommand.equals("off")) { + configManager.setSuggestedResponsePresetsEnabled(false); + sender.sendMessage(ChatColor.RED + configManager.getMessage("suggests-presets-disabled", null)); + return true; + } else { + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null)); + return true; + } + } + sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null)); + return true; + } +} + + + + + + + + diff --git a/src/main/java/com/ollamachat/command/OllamaChatTabCompleter.java b/src/main/java/com/ollamachat/command/OllamaChatTabCompleter.java new file mode 100644 index 0000000..7f8acd4 --- /dev/null +++ b/src/main/java/com/ollamachat/command/OllamaChatTabCompleter.java @@ -0,0 +1,143 @@ +package com.ollamachat.command; + +import com.ollamachat.core.Ollamachat; +import com.ollamachat.core.ConfigManager; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OllamaChatTabCompleter implements TabCompleter { + private final Ollamachat plugin; + private final ConfigManager configManager; + + public OllamaChatTabCompleter(Ollamachat plugin) { + this.plugin = plugin; + this.configManager = plugin.getConfigManager(); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List completions = new ArrayList<>(); + + if (command.getName().equalsIgnoreCase("ollamachat")) { + if (args.length == 1) { + List subCommands = new ArrayList<>(); + if (sender.hasPermission("ollamachat.reload")) { + subCommands.add("reload"); + } + if (sender.hasPermission("ollamachat.toggle")) { + subCommands.add("toggle"); + } + if (sender.hasPermission("ollamachat.prompt.set") || + sender.hasPermission("ollamachat.prompt.delete") || + sender.hasPermission("ollamachat.prompt.list") || + sender.hasPermission("ollamachat.prompt.select")) { + subCommands.add("prompt"); + } + if (sender instanceof Player && ( + sender.hasPermission("ollamachat.conversation.new") || + sender.hasPermission("ollamachat.conversation.select") || + sender.hasPermission("ollamachat.conversation.delete") || + sender.hasPermission("ollamachat.conversation.list"))) { + subCommands.add("conversation"); + } + if (sender instanceof Player && sender.hasPermission("ollamachat.suggests.toggle")) { + subCommands.add("suggests"); + } + if (sender.hasPermission("ollamachat.suggests-presets.toggle")) { + subCommands.add("suggests-presets"); + } + return filterCompletions(subCommands, args[0]); + } else if (args.length == 2 && args[0].equalsIgnoreCase("toggle") && sender.hasPermission("ollamachat.toggle")) { + List aiNames = new ArrayList<>(); + aiNames.add("ollama"); + aiNames.addAll(configManager.getOtherAIConfigs().keySet()); + return filterCompletions(aiNames, args[1]); + } else if (args.length == 2 && args[0].equalsIgnoreCase("prompt") && ( + sender.hasPermission("ollamachat.prompt.set") || + sender.hasPermission("ollamachat.prompt.delete") || + sender.hasPermission("ollamachat.prompt.list") || + sender.hasPermission("ollamachat.prompt.select"))) { + List promptSubCommands = new ArrayList<>(); + if (sender.hasPermission("ollamachat.prompt.set")) { + promptSubCommands.add("set"); + } + if (sender.hasPermission("ollamachat.prompt.delete")) { + promptSubCommands.add("delete"); + } + if (sender.hasPermission("ollamachat.prompt.list")) { + promptSubCommands.add("list"); + } + if (sender.hasPermission("ollamachat.prompt.select")) { + promptSubCommands.add("select"); + promptSubCommands.add("clear"); + } + return filterCompletions(promptSubCommands, args[1]); + } else if (args.length == 3 && args[0].equalsIgnoreCase("prompt") && ( + args[1].equalsIgnoreCase("delete") || args[1].equalsIgnoreCase("select")) && + (sender.hasPermission("ollamachat.prompt.delete") || sender.hasPermission("ollamachat.prompt.select"))) { + return filterCompletions(new ArrayList<>(configManager.getPrompts().keySet()), args[2]); + } else if (args.length == 2 && args[0].equalsIgnoreCase("conversation") && sender instanceof Player && + (sender.hasPermission("ollamachat.conversation.new") || + sender.hasPermission("ollamachat.conversation.select") || + sender.hasPermission("ollamachat.conversation.delete") || + sender.hasPermission("ollamachat.conversation.list"))) { + List convSubCommands = new ArrayList<>(); + if (sender.hasPermission("ollamachat.conversation.new")) { + convSubCommands.add("new"); + } + if (sender.hasPermission("ollamachat.conversation.select")) { + convSubCommands.add("select"); + } + if (sender.hasPermission("ollamachat.conversation.delete")) { + convSubCommands.add("delete"); + } + if (sender.hasPermission("ollamachat.conversation.list")) { + convSubCommands.add("list"); + } + return filterCompletions(convSubCommands, args[1]); + } else if (args.length == 3 && args[0].equalsIgnoreCase("conversation") && sender instanceof Player) { + List aiNames = new ArrayList<>(); + aiNames.add("ollama"); + aiNames.addAll(configManager.getOtherAIConfigs().keySet()); + return filterCompletions(aiNames, args[2]); + } else if (args.length == 4 && args[0].equalsIgnoreCase("conversation") && + (args[1].equalsIgnoreCase("select") || args[1].equalsIgnoreCase("delete")) && sender instanceof Player && + (sender.hasPermission("ollamachat.conversation.select") || sender.hasPermission("ollamachat.conversation.delete"))) { + String aiName = args[2]; + Map conversations = plugin.getChatHistoryManager().listConversations(((Player) sender).getUniqueId(), aiName); + return filterCompletions(new ArrayList<>(conversations.values()), args[3]); + } else if (args.length == 2 && (args[0].equalsIgnoreCase("suggests") || args[0].equalsIgnoreCase("suggests-presets")) && sender instanceof Player && + (sender.hasPermission("ollamachat.suggests.toggle") || sender.hasPermission("ollamachat.suggests-presets.toggle"))) { + return filterCompletions(Arrays.asList("on", "off"), args[1]); + } + } else if (command.getName().equalsIgnoreCase("aichat") && sender.hasPermission("ollamachat.use")) { + if (args.length == 1) { + List aiNames = new ArrayList<>(); + aiNames.add("ollama"); + aiNames.addAll(configManager.getOtherAIConfigs().keySet()); + return filterCompletions(aiNames, args[0]); + } + } + + return completions; + } + + private List filterCompletions(List options, String input) { + return options.stream() + .filter(option -> option.toLowerCase().startsWith(input.toLowerCase())) + .collect(Collectors.toList()); + } +} + + + + + diff --git a/src/main/java/com/ollamachat/core/ConfigManager.java b/src/main/java/com/ollamachat/core/ConfigManager.java new file mode 100644 index 0000000..0080af1 --- /dev/null +++ b/src/main/java/com/ollamachat/core/ConfigManager.java @@ -0,0 +1,325 @@ +package com.ollamachat.core; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; + +public class ConfigManager { + private final Ollamachat plugin; + private final Gson gson; + private JsonObject langConfig; + private String ollamaApiUrl; + private String ollamaModel; + private List triggerPrefixes; + private int maxResponseLength; + private Map otherAIConfigs; + private boolean ollamaEnabled; + private Map otherAIEnabled; + private boolean streamingEnabled; + private String defaultPrompt; + private Map prompts; + private Map> selectedConversations; + private int maxHistory; + private List suggestedResponseModels; + private boolean suggestedResponsesEnabled; + private int suggestedResponseCount; + private String suggestedResponsePrompt; + private List suggestedResponsePresets; + private Map suggestedResponseModelToggles; + private int suggestedResponseCooldown; + private boolean suggestedResponsePresetsEnabled; + + public ConfigManager(Ollamachat plugin) { + this.plugin = plugin; + this.gson = new Gson(); + this.selectedConversations = new HashMap<>(); + this.suggestedResponseModelToggles = new HashMap<>(); + } + + public void initialize() { + plugin.saveDefaultConfig(); + reloadConfigValues(); + loadLanguageFile(plugin.getConfig().getString("language", "en_us")); + } + + private void updateConfig() { + FileConfiguration config = plugin.getConfig(); + + if (!config.contains("ollama-enabled")) config.set("ollama-enabled", true); + if (!config.contains("language")) config.set("language", "en_us"); + if (!config.contains("other-ai-configs")) config.createSection("other-ai-configs"); + if (!config.contains("max-history")) config.set("max-history", 5); + if (!config.contains("stream-settings")) config.set("stream-settings.enabled", true); + if (!config.contains("prompts")) config.createSection("prompts"); + if (!config.contains("default-prompt")) config.set("default-prompt", ""); + if (!config.contains("trigger-prefixes")) { + config.set("trigger-prefixes", Arrays.asList("@bot", "@ai")); + } + if (!config.contains("suggested-response-models")) { + config.set("suggested-response-models", Arrays.asList("llama3")); + } + if (!config.contains("suggested-responses-enabled")) config.set("suggested-responses-enabled", true); + if (!config.contains("suggested-response-count")) config.set("suggested-response-count", 3); + if (!config.contains("suggested-response-prompt")) { + config.set("suggested-response-prompt", "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3"); + } + if (!config.contains("suggested-response-presets")) { + config.set("suggested-response-presets", Arrays.asList("I see what you mean.", "That's interesting!", "Tell me more about that.")); + } + if (!config.contains("suggested-response-model-toggles")) { + config.createSection("suggested-response-model-toggles"); + for (String model : config.getStringList("suggested-response-models")) { + if (!config.contains("suggested-response-model-toggles." + model)) { + config.set("suggested-response-model-toggles." + model, true); + } + } + } + if (!config.contains("suggested-response-cooldown")) config.set("suggested-response-cooldown", 10); + if (!config.contains("suggested-response-presets-enabled")) config.set("suggested-response-presets-enabled", true); + if (!config.contains("database")) { + config.set("database.type", "sqlite"); + config.set("database.mysql.host", "localhost"); + config.set("database.mysql.port", 3306); + config.set("database.mysql.database", "ollamachat"); + config.set("database.mysql.username", "root"); + config.set("database.mysql.password", ""); + } + + plugin.saveConfig(); + } + + public void reloadConfigValues() { + File configFile = new File(plugin.getDataFolder(), "config.yml"); + if (!configFile.exists()) { + plugin.saveDefaultConfig(); + } else { + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + if (!config.contains("ollama-api-url") || !config.contains("model")) { + plugin.getLogger().warning(getMessage("config-invalid", null)); + configFile.delete(); + plugin.saveDefaultConfig(); + } + } catch (Exception e) { + plugin.getLogger().severe(getMessage("config-load-failed", Map.of("error", e.getMessage()))); + configFile.delete(); + plugin.saveDefaultConfig(); + } + } + + plugin.reloadConfig(); + updateConfig(); + + FileConfiguration config = plugin.getConfig(); + ollamaApiUrl = config.getString("ollama-api-url", "http://localhost:11434/api/generate"); + ollamaModel = config.getString("model", "llama3"); + triggerPrefixes = config.getStringList("trigger-prefixes"); + maxResponseLength = config.getInt("max-response-length", 500); + ollamaEnabled = config.getBoolean("ollama-enabled", true); + maxHistory = config.getInt("max-history", 5); + streamingEnabled = config.getBoolean("stream-settings.enabled", true); + defaultPrompt = config.getString("default-prompt", ""); + suggestedResponseModels = config.getStringList("suggested-response-models"); + suggestedResponsesEnabled = config.getBoolean("suggested-responses-enabled", true); + suggestedResponseCount = config.getInt("suggested-response-count", 3); + suggestedResponsePrompt = config.getString("suggested-response-prompt", "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3"); + suggestedResponsePresets = config.getStringList("suggested-response-presets"); + suggestedResponseCooldown = config.getInt("suggested-response-cooldown", 10); + suggestedResponsePresetsEnabled = config.getBoolean("suggested-response-presets-enabled", true); + + prompts = new HashMap<>(); + if (config.contains("prompts") && config.getConfigurationSection("prompts") != null) { + for (String promptName : config.getConfigurationSection("prompts").getKeys(false)) { + String promptContent = config.getString("prompts." + promptName); + prompts.put(promptName, promptContent); + } + } + + otherAIConfigs = new HashMap<>(); + otherAIEnabled = new HashMap<>(); + if (config.contains("other-ai-configs") && config.getConfigurationSection("other-ai-configs") != null) { + for (String aiName : config.getConfigurationSection("other-ai-configs").getKeys(false)) { + String apiUrl = config.getString("other-ai-configs." + aiName + ".api-url"); + String apiKey = config.getString("other-ai-configs." + aiName + ".api-key"); + String model = config.getString("other-ai-configs." + aiName + ".model"); + boolean enabled = config.getBoolean("other-ai-configs." + aiName + ".enabled", true); + boolean isMessagesFormat = config.getBoolean("other-ai-configs." + aiName + ".messages-format", false); + otherAIConfigs.put(aiName, new AIConfig(apiUrl, apiKey, model, isMessagesFormat)); + otherAIEnabled.put(aiName, enabled); + } + } + + suggestedResponseModelToggles = new HashMap<>(); + if (config.contains("suggested-response-model-toggles") && config.getConfigurationSection("suggested-response-model-toggles") != null) { + for (String model : config.getConfigurationSection("suggested-response-model-toggles").getKeys(false)) { + suggestedResponseModelToggles.put(model, config.getBoolean("suggested-response-model-toggles." + model, true)); + } + } + } + + private void loadLanguageFile(String language) { + File langFolder = new File(plugin.getDataFolder(), "lang"); + if (!langFolder.exists()) { + langFolder.mkdirs(); + } + + File langFile = new File(langFolder, language + ".json"); + if (!langFile.exists()) { + plugin.saveResource("lang/" + language + ".json", false); + } + + try (FileReader reader = new FileReader(langFile)) { + langConfig = gson.fromJson(reader, JsonObject.class); + if (langConfig == null) { + langConfig = new JsonObject(); + plugin.getLogger().warning("Language file is empty or invalid: " + langFile.getName()); + } + } catch (IOException e) { + plugin.getLogger().severe("Failed to load language file: " + langFile.getName() + " - " + e.getMessage()); + langConfig = new JsonObject(); + } + } + + public String getMessage(String key, Map placeholders) { + String message = langConfig != null && langConfig.has(key) ? langConfig.get(key).getAsString() : "§c[OllamaChat] Missing language key: " + key; + if (placeholders != null) { + for (Map.Entry entry : placeholders.entrySet()) { + message = message.replace("{" + entry.getKey() + "}", entry.getValue()); + } + } + return message; + } + + public boolean isSuggestedResponsePresetsEnabled() { + return suggestedResponsePresetsEnabled; + } + + public void setSuggestedResponsePresetsEnabled(boolean enabled) { + this.suggestedResponsePresetsEnabled = enabled; + plugin.getConfig().set("suggested-response-presets-enabled", enabled); + plugin.saveConfig(); + } + + // Getters + public String getOllamaApiUrl() { + return ollamaApiUrl; + } + + public String getOllamaModel() { + return ollamaModel; + } + + public List getTriggerPrefixes() { + return triggerPrefixes; + } + + public int getMaxResponseLength() { + return maxResponseLength; + } + + public Map getOtherAIConfigs() { + return otherAIConfigs; + } + + public boolean isOllamaEnabled() { + return ollamaEnabled; + } + + public void setOllamaEnabled(boolean enabled) { + this.ollamaEnabled = enabled; + } + + public Map getOtherAIEnabled() { + return otherAIEnabled; + } + + public boolean isStreamingEnabled() { + return streamingEnabled; + } + + public String getDefaultPrompt() { + return defaultPrompt; + } + + public void setDefaultPrompt(String prompt) { + this.defaultPrompt = prompt; + } + + public Map getPrompts() { + return prompts; + } + + public Map> getSelectedConversations() { + return selectedConversations; + } + + public int getMaxHistory() { + return maxHistory; + } + + public List getSuggestedResponseModels() { + return suggestedResponseModels; + } + + public boolean isSuggestedResponsesEnabled() { + return suggestedResponsesEnabled; + } + + public int getSuggestedResponseCount() { + return suggestedResponseCount; + } + + public String getSuggestedResponsePrompt() { + return suggestedResponsePrompt; + } + + public List getSuggestedResponsePresets() { + return suggestedResponsePresets; + } + + public Map getSuggestedResponseModelToggles() { + return suggestedResponseModelToggles; + } + + public int getSuggestedResponseCooldown() { + return suggestedResponseCooldown; + } + + public static class AIConfig { + private final String apiUrl; + private final String apiKey; + private final String model; + private final boolean isMessagesFormat; + + public AIConfig(String apiUrl, String apiKey, String model, boolean isMessagesFormat) { + this.apiUrl = apiUrl; + this.apiKey = apiKey; + this.model = model; + this.isMessagesFormat = isMessagesFormat; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getApiKey() { + return apiKey; + } + + public String getModel() { + return model; + } + + public boolean isMessagesFormat() { + return isMessagesFormat; + } + } +} + + diff --git a/src/main/java/com/ollamachat/core/Ollamachat.java b/src/main/java/com/ollamachat/core/Ollamachat.java new file mode 100644 index 0000000..9cc4f64 --- /dev/null +++ b/src/main/java/com/ollamachat/core/Ollamachat.java @@ -0,0 +1,112 @@ + +package com.ollamachat.core; + +import com.ollamachat.ChatHistoryManager; +import com.ollamachat.DatabaseManager; +import com.ollamachat.DependencyLoader; +import com.ollamachat.ProgressManager; +import com.ollamachat.chat.ChatTriggerHandler; +import com.ollamachat.chat.SuggestedResponseHandler; +import com.ollamachat.command.AIChatCommand; +import com.ollamachat.command.OllamaChatCommand; +import com.ollamachat.command.OllamaChatTabCompleter; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Ollamachat extends JavaPlugin { + private ConfigManager configManager; + private DatabaseManager databaseManager; + private ChatHistoryManager chatHistoryManager; + private ProgressManager progressManager; + private SuggestedResponseHandler suggestedResponseHandler; + private Map playerSuggestionToggles; + + @Override + public void onEnable() { + // Load dependencies first + DependencyLoader dependencyLoader = new DependencyLoader(this); + ClassLoader dependencyClassLoader; + try { + dependencyLoader.loadDependencies(); + dependencyClassLoader = dependencyLoader.getClass().getClassLoader(); + } catch (Exception e) { + getLogger().severe("Failed to load dependencies: " + e.getMessage()); + getServer().getPluginManager().disablePlugin(this); + return; + } + + configManager = new ConfigManager(this); + configManager.initialize(); + + try { + databaseManager = new DatabaseManager(this, dependencyClassLoader); + } catch (Exception e) { + getLogger().severe("Failed to initialize DatabaseManager: " + e.getMessage()); + getServer().getPluginManager().disablePlugin(this); + return; + } + + int maxHistory = configManager.getMaxHistory(); + chatHistoryManager = new ChatHistoryManager(databaseManager, maxHistory); + progressManager = new ProgressManager(this); + suggestedResponseHandler = new SuggestedResponseHandler(this); + playerSuggestionToggles = new HashMap<>(); + + // Register events + getServer().getPluginManager().registerEvents(new ChatTriggerHandler(this), this); + + // Register commands + getCommand("ollamachat").setExecutor(new OllamaChatCommand(this)); + getCommand("ollamachat").setTabCompleter(new OllamaChatTabCompleter(this)); + getCommand("aichat").setExecutor(new AIChatCommand(this)); + getCommand("aichat").setTabCompleter(new OllamaChatTabCompleter(this)); + } + + @Override + public void onDisable() { + if (databaseManager != null) { + databaseManager.close(); + } else { + getLogger().warning("DatabaseManager was null, skipping close."); + } + if (progressManager != null) { + getServer().getOnlinePlayers().forEach(progressManager::cleanup); + } + } + + // Getter methods + public ConfigManager getConfigManager() { + return configManager; + } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public ChatHistoryManager getChatHistoryManager() { + return chatHistoryManager; + } + + public ProgressManager getProgressManager() { + return progressManager; + } + + public SuggestedResponseHandler getSuggestedResponseHandler() { + return suggestedResponseHandler; + } + + public Map getPlayerSuggestionToggles() { + return playerSuggestionToggles; + } +} + + + + + + + + diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c96b93b..0f153ff 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,9 +3,14 @@ ollama-api-url: "http://localhost:11434/api/generate" model: "llama3" ollama-enabled: true +# Streaming settings +stream-settings: + enabled: true # Whether to enable streaming + # Chat -trigger-prefix: "@bot " -response-prefix: "§b[AI] §r" +trigger-prefixes: + - "@bot" + - "@ai" # Length max-response-length: 500 @@ -14,7 +19,50 @@ max-response-length: 500 max-history: 5 # Language Settings -language: "en" +language: "en_us" + +# Progress Display Settings +progress-display: + enabled: true # Whether to enable progress display + type: "bossbar" # Display type (bossbar or actionbar) + color: "BLUE" # BossBar color (BLUE, GREEN, RED, etc.) + style: "SOLID" # BossBar style (SOLID, SEGMENTED_6, etc.) + update-interval: 1 # Progress update frequency (in seconds) + +# Suggested Response +suggested-responses-enabled: false +suggested-response-models: + - "llama3" +suggested-response-count: 3 +suggested-response-prompt: "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3" +suggested-response-presets: + - "I see what you mean." + - "That's interesting!" + - "Tell me more about that." +suggested-response-model-toggles: + - llama3 +suggested-response-cooldown: 10 +suggested-response-presets-enabled: false + +# Database (for mysql, set database.type: mysql.) +database: + type: sqlite + mysql: + host: localhost + port: 3306 + database: ollamachat + username: root + password: "" + + +# Default prompt to prepend to user inputs (empty for none) +default-prompt: "" + +# Custom prompts +prompts: +# Example: +# friendly: "You are a friendly assistant who responds in a cheerful tone." +# formal: "You are a professional assistant who responds formally." # Other AI Configurations other-ai-configs: @@ -22,5 +70,6 @@ other-ai-configs: api-url: "https://api.openai.com/v1/chat/completions" api-key: "your-openai-api-key" model: "gpt-4" - enabled: true + enabled: false + messages-format: true diff --git a/src/main/resources/lang/en.lang b/src/main/resources/lang/en.lang deleted file mode 100644 index d1a1179..0000000 --- a/src/main/resources/lang/en.lang +++ /dev/null @@ -1,18 +0,0 @@ -# General messages -reload-success: "§aConfiguration reloaded." -toggle-enabled: "§a{ai-name} is now enabled." -toggle-disabled: "§a{ai-name} is now disabled." -invalid-ai-name: "§cInvalid AI name. Available AIs: {ai-list}." -usage-aichat: "§cUsage: /aichat " -usage-ollamachat: "§cUsage: /ollamachat >" -player-only: "§cThis command can only be used by players." -ollama-enabled: "§aOllama is now enabled." -ollama-disabled: "§aOllama is now disabled." - -# Chat interaction -response-prefix: "§b[AI] §r" -error-prefix: "§c[Error] §r" - -# Log messages -config-invalid: "§cConfig file is invalid or incomplete. Regenerating default config..." -config-load-failed: "§cFailed to load config file: {error}. Regenerating default config..." \ No newline at end of file diff --git a/src/main/resources/lang/en_us.json b/src/main/resources/lang/en_us.json new file mode 100644 index 0000000..48640ed --- /dev/null +++ b/src/main/resources/lang/en_us.json @@ -0,0 +1,51 @@ +{ + "usage-ollamachat": "§e[OllamaChat] Usage:\n- /ollamachat reload\n- /ollamachat toggle \n- /ollamachat prompt [args]\n- /ollamachat conversation [args]", + "usage-aichat": "§e[OllamaChat] Usage:\n- /aichat ", + "reload-success": "§a[OllamaChat] Configuration reloaded successfully!", + "ollama-enabled": "§a[OllamaChat] Ollama integration enabled!", + "ollama-disabled": "§c[OllamaChat] Ollama integration disabled!", + "toggle-enabled": "§a[OllamaChat] {ai-name} integration enabled!", + "toggle-disabled": "§c[OllamaChat] {ai-name} integration disabled!", + "invalid-ai-name": "§c[OllamaChat] Invalid AI name! Available AIs: {ai-list}", + "player-only": "§c[OllamaChat] This command can only be used by players!", + "config-invalid": "§c[OllamaChat] Configuration file is invalid, resetting to default.", + "config-load-failed": "§c[OllamaChat] Failed to load configuration: {error}", + "response-prefix": "§7[OllamaChat] §f", + "error-prefix": "§c[OllamaChat] §f", + "generating-status": "§a[OllamaChat] Generating... ({progress}%)", + "complete-status": "§a[OllamaChat] Completed!", + "error-status": "§c[OllamaChat] Error!", + "no-permission": "§c[OllamaChat] You do not have permission to use this command!", + "prompt-usage": "§e[OllamaChat] Prompt Usage:\n- /ollamachat prompt set \n- /ollamachat prompt delete \n- /ollamachat prompt list\n- /ollamachat prompt select \n- /ollamachat prompt clear", + "prompt-set": "§a[OllamaChat] Prompt '{name}' set successfully!", + "prompt-deleted": "§a[OllamaChat] Prompt '{name}' deleted successfully!", + "prompt-not-found": "§c[OllamaChat] Prompt '{name}' not found!", + "prompt-list": "§a[OllamaChat] Available prompts:\n- {prompts}", + "prompt-list-empty": "§a[OllamaChat] No prompts available.", + "prompt-selected": "§a[OllamaChat] Default prompt set to '{name}'.", + "prompt-cleared": "§a[OllamaChat] Default prompt cleared.", + "prompt-default": "§a[OllamaChat] Current default prompt: '{name}'", + "prompt-default-invalid": "§c[OllamaChat] Current default prompt '{name}' is invalid (not found).", + "conversation-usage": "§e[OllamaChat] Conversation Usage:\n- /ollamachat conversation new \n- /ollamachat conversation select \n- /ollamachat conversation delete \n- /ollamachat conversation list ", + "conversation-created": "§a[OllamaChat] Conversation '{name}' created for {ai-name}!", + "conversation-selected": "§a[OllamaChat] Selected conversation '{name}' for {ai-name}.", + "conversation-deleted": "§a[OllamaChat] Conversation '{name}' deleted for {ai-name}.", + "conversation-not-found": "§c[OllamaChat] Conversation '{name}' not found!", + "conversation-list": "§a[OllamaChat] Available conversations for {ai-name}:\n- {conversations}", + "conversation-list-empty": "§a[OllamaChat] No conversations available for {ai-name}.", + "conversation-default": "§a[OllamaChat] Current conversation for {ai-name}: '{name}'", + "conversation-default-invalid": "§c[OllamaChat] Current conversation '{name}' for {ai-name} is invalid (not found).", + "suggested-responses-header": "§a[OllamaChat] Suggested Responses:", + "suggested-response-prefix": "§7[Click to Reply] §f", + "general-error": "§c[OllamaChat] An unexpected error occurred: {error}", + "streaming-interrupted": "§c[OllamaChat] Streaming response was interrupted.", + "database-error": "§c[OllamaChat] Database error occurred: {error}", + "response-truncated": "§e[OllamaChat] Response was truncated due to length limit.", + "json-render-error": "§c[OllamaChat] Failed to render suggested response: {error}", + "suggests-enabled": "§a[OllamaChat] Suggested responses enabled for {player}!", + "suggests-disabled": "§c[OllamaChat] Suggested responses disabled for {player}!", + "suggests-rate-limit": "§c[OllamaChat] Please wait {seconds} seconds before generating more suggestions.", + "suggested-response-hover": "§eClick to send this suggestion as a prompt!", + "suggests-presets-enabled": "§a[OllamaChat] Suggested response presets have been enabled.", + "suggests-presets-disabled": "§c[OllamaChat] Suggested response presets have been disabled." +} \ No newline at end of file diff --git a/src/main/resources/lang/zh_cn.json b/src/main/resources/lang/zh_cn.json new file mode 100644 index 0000000..4233874 --- /dev/null +++ b/src/main/resources/lang/zh_cn.json @@ -0,0 +1,52 @@ +{ + "usage-ollamachat": "§e[OllamaChat] 用法说明:\n- /ollamachat reload\n- /ollamachat toggle \n- /ollamachat prompt [参数]\n- /ollamachat conversation [参数]", + "usage-aichat": "§e[OllamaChat] 用法说明:\n- /aichat <提示内容>", + "reload-success": "§a[OllamaChat] 配置已成功重新加载!", + "ollama-enabled": "§a[OllamaChat] Ollama 集成功能已启用!", + "ollama-disabled": "§c[OllamaChat] Ollama 集成功能已停用!", + "toggle-enabled": "§a[OllamaChat] {ai-name} 集成功能已启用!", + "toggle-disabled": "§c[OllamaChat] {ai-name} 集成功能已停用!", + "invalid-ai-name": "§c[OllamaChat] 无效的 AI 名称!可用 AI 列表:{ai-list}", + "player-only": "§c[OllamaChat] 此命令仅限玩家使用!", + "config-invalid": "§c[OllamaChat] 配置文件无效,已重置为默认设置。", + "config-load-failed": "§c[OllamaChat] 加载配置失败:{error}", + "response-prefix": "§7[OllamaChat] §f", + "error-prefix": "§c[OllamaChat] §f", + "generating-status": "§a[OllamaChat] 生成中... ({progress}%)", + "complete-status": "§a[OllamaChat] 已完成!", + "error-status": "§c[OllamaChat] 出错!", + "no-permission": "§c[OllamaChat] 您没有权限使用此命令!", + "prompt-usage": "§e[OllamaChat] 提示词用法说明:\n- /ollamachat prompt set <名称> <内容>\n- /ollamachat prompt delete <名称>\n- /ollamachat prompt list\n- /ollamachat prompt select <名称>\n- /ollamachat prompt clear", + "prompt-set": "§a[OllamaChat] 提示词 '{name}' 已设置成功!", + "prompt-deleted": "§a[OllamaChat] 提示词 '{name}' 已删除成功!", + "prompt-not-found": "§c[OllamaChat] 未找到提示词 '{name}'!", + "prompt-list": "§a[OllamaChat] 可用提示词列表:\n- {prompts}", + "prompt-list-empty": "§a[OllamaChat] 暂无可用提示词。", + "prompt-selected": "§a[OllamaChat] 已将 '{name}' 设为默认提示词。", + "prompt-cleared": "§a[OllamaChat] 默认提示词已清除。", + "prompt-default": "§a[OllamaChat] 当前默认提示词:'{name}'", + "prompt-default-invalid": "§c[OllamaChat] 当前默认提示词 '{name}' 无效(未找到)。", + "conversation-usage": "§e[OllamaChat] 对话管理用法说明:\n- /ollamachat conversation new <名称>\n- /ollamachat conversation select \n- /ollamachat conversation delete \n- /ollamachat conversation list ", + "conversation-created": "§a[OllamaChat] 已为 {ai-name} 创建对话 '{name}'!", + "conversation-selected": "§a[OllamaChat] 已为 {ai-name} 选择对话 '{name}'。", + "conversation-deleted": "§a[OllamaChat] 已为 {ai-name} 删除对话 '{name}'。", + "conversation-not-found": "§c[OllamaChat] 未找到对话 '{name}'!", + "conversation-list": "§a[OllamaChat] {ai-name} 的可用对话列表:\n- {conversations}", + "conversation-list-empty": "§a[OllamaChat] {ai-name} 暂无可用对话。", + "conversation-default": "§a[OllamaChat] {ai-name} 的当前对话:'{name}'", + "conversation-default-invalid": "§c[OllamaChat] {ai-name} 的当前对话 '{name}' 无效(未找到)。", + "suggested-responses-header": "§a[OllamaChat] 推荐回复:", + "suggested-response-prefix": "§7[点击回复] §f", + "general-error": "§c[OllamaChat] 发生意外错误:{error}", + "streaming-interrupted": "§c[OllamaChat] 流式响应已中断。", + "database-error": "§c[OllamaChat] 数据库错误:{error}", + "response-truncated": "§e[OllamaChat] 响应因长度限制已被截断。", + "json-render-error": "§c[OllamaChat] 未能渲染成功推荐回复: {error}", + "suggests-enabled": "§a[OllamaChat] 已为玩家{player}启用推荐回复!", + "suggests-disabled": "§c[OllamaChat] 已为玩家{player}禁用推荐回复!", + "suggests-rate-limit": "§c[OllamaChat] 请等待{seconds}秒后再生成推荐回复", + "suggested-response-hover": "§e点击发送此推荐内容!", + "suggests-presets-enabled": "§a[OllamaChat] 已启用预设推荐回复", + "suggests-presets-disabled": "§c[OllamaChat] 已启用预设推荐回复" +} + diff --git a/src/main/resources/lang/zh_cn.lang b/src/main/resources/lang/zh_cn.lang deleted file mode 100644 index c2389c2..0000000 --- a/src/main/resources/lang/zh_cn.lang +++ /dev/null @@ -1,18 +0,0 @@ -# 通用消息 -reload-success: "§a配置已重新加载。" -toggle-enabled: "§a{ai-name} 已启用。" -toggle-disabled: "§a{ai-name} 已禁用。" -invalid-ai-name: "§c无效的 AI 名称。可用的 AI: {ai-list}。" -usage-aichat: "§c用法: /aichat <提示>" -usage-ollamachat: "§c用法: /ollamachat >" -player-only: "§c该命令只能由玩家使用。" -ollama-enabled: "§aOllama 已启用。" -ollama-disabled: "§aOllama 已禁用。" - -# 聊天交互 -response-prefix: "§b[AI] §r" -error-prefix: "§c[错误] §r" - -# 日志消息 -config-invalid: "§c配置文件无效或不完整,正在重新生成默认配置..." -config-load-failed: "§c加载配置文件失败: {error},正在重新生成默认配置..." \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 8773e01..438745e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,25 +1,58 @@ name: OllamaChat -version: '1.0.2' -main: com.ollamachat.Ollamachat +version: '1.1.4' +main: com.ollamachat.core.Ollamachat api-version: '1.21' authors: [xwwsdd] -description: A plugin used to connect Ollama with Minecraft. +description: A plugin used to connect Ollama and OtherAI with Minecraft. website: https://chat.sarskin.cn/invite/iHgI6LTX commands: ollamachat: description: Manage OllamaChat plugin (reload configuration or toggle AI) usage: "{usage-ollamachat}" - permission: ollamachat.admin + permission: ollamachat.reload aichat: description: Interact with other AI services usage: "{usage-aichat}" permission: ollamachat.use permissions: - ollamachat.admin: - description: Allows managing the plugin (reload and toggle AI) + ollamachat.reload: + description: Allows managing the plugin (reload) + default: op + ollamachat.toggle: + description: Allows managing the plugin (toggle AI) default: op ollamachat.use: description: Allows using the /aichat command - default: true \ No newline at end of file + default: true + ollamachat.prompt.set: + description: Allows setting a new prompt + default: op + ollamachat.prompt.delete: + description: Allows deleting a prompt + default: op + ollamachat.prompt.list: + description: Allows listing all prompts + default: op + ollamachat.prompt.select: + description: Allows selecting or clearing a prompt + default: op + ollamachat.conversation.new: + description: Allows creating a new conversation + default: true + ollamachat.conversation.select: + description: Allows selecting a conversation + default: true + ollamachat.conversation.list: + description: Allows listing conversations + default: true + ollamachat.conversation.delete: + description: Allows deleting a conversation + default: true + ollamachat.suggests.toggle: + description: Allows toggling suggested responses on or off + default: true + ollamachat.suggests-presets.toggle: + description: Allows toggling of suggested response presets + default: op \ No newline at end of file