From 8612efdc36153e99bb5f8ff402b75c595289b64a Mon Sep 17 00:00:00 2001 From: piitex Date: Fri, 7 Nov 2025 15:55:54 -0600 Subject: [PATCH 01/22] Removed unused chat view cache. --- src/main/java/me/piitex/app/backend/Chat.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/me/piitex/app/backend/Chat.java b/src/main/java/me/piitex/app/backend/Chat.java index 976c0f8e..d7083705 100644 --- a/src/main/java/me/piitex/app/backend/Chat.java +++ b/src/main/java/me/piitex/app/backend/Chat.java @@ -17,7 +17,6 @@ public class Chat { private Response response; private final LinkedList messages = new LinkedList<>(); private final boolean dev = false; - private ChatView cachedView; public Chat(File file) { @@ -240,12 +239,4 @@ public Response getResponse() { public void setResponse(Response response) { this.response = response; } - - public ChatView getCachedView() { - return cachedView; - } - - public void setCachedView(ChatView cachedView) { - this.cachedView = cachedView; - } } From 668065866611e7f1007038bd4a1433c32a2113b0 Mon Sep 17 00:00:00 2001 From: piitex Date: Fri, 7 Nov 2025 15:56:25 -0600 Subject: [PATCH 02/22] Enabled window scaling. --- src/main/java/me/piitex/app/App.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/piitex/app/App.java b/src/main/java/me/piitex/app/App.java index 3cc4b58d..eaeaf2e1 100644 --- a/src/main/java/me/piitex/app/App.java +++ b/src/main/java/me/piitex/app/App.java @@ -160,7 +160,7 @@ public void initialization(Stage initialStage) { // This is because the pathing for the image doesn't change but the image gets replaced by the new image. ImageLoader.useCache = false; - window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale(false).setDimensions(setWidth, setHeight).build(); + window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale(true).setDimensions(setWidth, setHeight).build(); // Initialize global positions. Needed for the rendering process. Positions.initialize(); From 08781bd251cbb08b564784da6deb0b26fe905aa9 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:17:07 -0600 Subject: [PATCH 03/22] Updated ren-engine -> 1.0.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af690af3..193012dc 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ me.piitex.engine ren-engine - 1.0.4-SNAPSHOT + 1.0.6-SNAPSHOT org.junit.jupiter From d9160a0b871d2cc08fa777837d244318615c292b Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:19:47 -0600 Subject: [PATCH 04/22] Added window scaling setting. --- .../app/views/settings/SettingsView.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/me/piitex/app/views/settings/SettingsView.java b/src/main/java/me/piitex/app/views/settings/SettingsView.java index 1215f330..f03ba9de 100644 --- a/src/main/java/me/piitex/app/views/settings/SettingsView.java +++ b/src/main/java/me/piitex/app/views/settings/SettingsView.java @@ -50,6 +50,7 @@ public void build() { scrollContainer.setHorizontalScroll(false); root.addElement(scrollContainer); + layout.addElement(buildWindowScaling()); layout.addElement(buildResolution()); layout.addElement(buildGlobalChatSize()); layout.addElement(buildChatSize()); @@ -59,6 +60,28 @@ public void build() { layout.addElement(buildAstrixColor()); } + public TileContainer buildWindowScaling() { + TileContainer tileContainer = new TileContainer(0, 0, appSettings.getWidth() - 300, 120); + tileContainer.setMaxSize(appSettings.getWidth() - 300, 120); + tileContainer.addStyle(Styles.BORDER_DEFAULT); + tileContainer.addStyle(Styles.BG_DEFAULT); + tileContainer.addStyle(appSettings.getGlobalTextSize()); + + tileContainer.setTitle("Window Scaling"); + tileContainer.setDescription("Enable/Disable window scaling. Can cause text to become blurry or stretched."); + + ToggleSwitchOverlay toggleSwitchOverlay = new ToggleSwitchOverlay(appSettings.isWindowScaling()); + toggleSwitchOverlay.onToggle(event -> { + App.window.clear(); + App.window.close(false); + appSettings.setWindowScaling(event.getNewValue()); + App.getInstance().initialization(App.window.getStage()); + }); + tileContainer.setAction(toggleSwitchOverlay); + + return tileContainer; + } + public TileContainer buildResolution() { TileContainer tileContainer = new TileContainer(0, 0, appSettings.getWidth() - 300, 120); tileContainer.setMaxSize(appSettings.getWidth() - 300, 120); From 3a4e52014b606b8b1b050b3c76b2579d7fa72cf5 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:20:14 -0600 Subject: [PATCH 05/22] Fixed issues with chat switching. --- src/main/java/me/piitex/app/views/chats/ChatView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/piitex/app/views/chats/ChatView.java b/src/main/java/me/piitex/app/views/chats/ChatView.java index b2b64af9..b54fec0c 100644 --- a/src/main/java/me/piitex/app/views/chats/ChatView.java +++ b/src/main/java/me/piitex/app/views/chats/ChatView.java @@ -199,7 +199,7 @@ public ChoiceBoxOverlay buildSelection() { ChoiceBox choiceBox = selection.getChoiceBox(); choiceBox.getSelectionModel().clearSelection(); App.window.clearContainers(); - ChatView cachedView = character.getChatViewCachedNodes().get(chat); + ChatView cachedView = character.getChatViewCachedNodes().get(next); if (next != null && cachedView != null) { App.logger.info("Using cached selection view..."); App.window.addContainer(cachedView); From 2193aff1649a0319f94f43023cd1fae2c24baf77 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:20:47 -0600 Subject: [PATCH 06/22] Adjusted map import. --- src/main/java/me/piitex/app/backend/Character.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/piitex/app/backend/Character.java b/src/main/java/me/piitex/app/backend/Character.java index 6cec8579..a861a437 100644 --- a/src/main/java/me/piitex/app/backend/Character.java +++ b/src/main/java/me/piitex/app/backend/Character.java @@ -4,7 +4,7 @@ import me.piitex.app.App; import me.piitex.app.configuration.ModelSettings; import me.piitex.app.views.chats.ChatView; -import me.piitex.engine.LimitedHashMap; +import me.piitex.engine.maps.LimitedHashMap; import me.piitex.os.configurations.InfoFile; import java.io.File; From 6924a4e84411b73210af65420ed7cdf93b0f8019 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:21:02 -0600 Subject: [PATCH 07/22] Added window scaling. --- src/main/java/me/piitex/app/App.java | 4 ++-- .../piitex/app/configuration/AppSettings.java | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/me/piitex/app/App.java b/src/main/java/me/piitex/app/App.java index eaeaf2e1..4316a975 100644 --- a/src/main/java/me/piitex/app/App.java +++ b/src/main/java/me/piitex/app/App.java @@ -160,7 +160,7 @@ public void initialization(Stage initialStage) { // This is because the pathing for the image doesn't change but the image gets replaced by the new image. ImageLoader.useCache = false; - window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale(true).setDimensions(setWidth, setHeight).build(); + window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale((appSettings.isWindowScaling()) && !mobile).setAntiAliasing(false).setDimensions(setWidth, setHeight).build(); // Initialize global positions. Needed for the rendering process. Positions.initialize(); @@ -373,7 +373,7 @@ public Map getUserTemplates() { public Window buildErrorWindow(String message) { if (window != null) { - window.close(); + window.close(true); } Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); diff --git a/src/main/java/me/piitex/app/configuration/AppSettings.java b/src/main/java/me/piitex/app/configuration/AppSettings.java index 32025325..2376f16c 100644 --- a/src/main/java/me/piitex/app/configuration/AppSettings.java +++ b/src/main/java/me/piitex/app/configuration/AppSettings.java @@ -16,6 +16,7 @@ public class AppSettings { private String textColor; private String quoteColor; private String astrixColor; + private boolean windowScaling = false; private final InfoFile infoFile; @@ -55,6 +56,9 @@ public AppSettings() { } else { this.astrixColor = Color.DODGERBLUE.toString(); } + if (infoFile.hasKey("window-scaling")) { + this.windowScaling = infoFile.getBoolean("window-scaling"); + } } public int getWidth() { @@ -142,10 +146,18 @@ public void setAstrixColor(String astrixColor) { infoFile.set("astrix-color", astrixColor); } + public boolean isWindowScaling() { + return windowScaling; + } + + public void setWindowScaling(boolean windowScaling) { + this.windowScaling = windowScaling; + } + /* - Utility functions for getting theme coloring. - Needed for RichTextFX components and BBCode - */ + Utility functions for getting theme coloring. + Needed for RichTextFX components and BBCode + */ public Theme getStyleTheme(String name) { if (name.equalsIgnoreCase("primer light")) { return new PrimerLight(); From ba335d9201f901e7c909a1c85e3d240a222ae963 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 14:50:02 -0600 Subject: [PATCH 08/22] Addressed qodana issues. --- src/main/java/me/piitex/app/backend/Chat.java | 1 - .../java/me/piitex/app/views/chats/components/ChatBoxCard.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/piitex/app/backend/Chat.java b/src/main/java/me/piitex/app/backend/Chat.java index d7083705..65390df8 100644 --- a/src/main/java/me/piitex/app/backend/Chat.java +++ b/src/main/java/me/piitex/app/backend/Chat.java @@ -1,7 +1,6 @@ package me.piitex.app.backend; import me.piitex.app.App; -import me.piitex.app.views.chats.ChatView; import me.piitex.os.configurations.FileCrypter; import javax.crypto.IllegalBlockSizeException; diff --git a/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java b/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java index 212dbbbb..3d7b4a3c 100644 --- a/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java +++ b/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java @@ -45,6 +45,7 @@ public void buildCard() { displayName = character.getUser().getDisplayName(); } else { iconPath = (character != null && character.getIconPath() != null && !character.getIconPath().isEmpty() ? character.getIconPath() : new File(App.getAppDirectory(), "icons/character.png").getAbsolutePath()); + assert character != null; // Character cannot be null at this stage. If the character is null, the error will be caught and thrown before this call. displayName = character.getDisplayName(); } From 19f6adb1d752d750f6ccd919b0024d2a4e19da67 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 17:24:26 -0600 Subject: [PATCH 09/22] Added character card exporter. --- .../app/utils/CharacterCardExporter.java | 106 ++++++++++++++++++ .../app/utils/CharacterCardImporter.java | 16 +++ .../me/piitex/app/utils/CharacterUtil.java | 39 +++++++ .../views/characters/tabs/CharacterTab.java | 20 ++++ 4 files changed, 181 insertions(+) create mode 100644 src/main/java/me/piitex/app/utils/CharacterCardExporter.java create mode 100644 src/main/java/me/piitex/app/utils/CharacterUtil.java diff --git a/src/main/java/me/piitex/app/utils/CharacterCardExporter.java b/src/main/java/me/piitex/app/utils/CharacterCardExporter.java new file mode 100644 index 00000000..08f0a0e2 --- /dev/null +++ b/src/main/java/me/piitex/app/utils/CharacterCardExporter.java @@ -0,0 +1,106 @@ +package me.piitex.app.utils; + +import me.piitex.app.App; +import me.piitex.app.backend.Character; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.Iterator; + +public class CharacterCardExporter { + private final Character character; + + public CharacterCardExporter(File output, Character character) { + this.character = character; + // Using swing for now + try { + Files.copy(new File(character.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + + BufferedImage image; + try { + image = ImageIO.read(output); + } catch (IOException e) { + App.logger.error("Could load character export output image.", e); + return; + } + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + App.logger.error("Could not find png writer for exporter.", new RuntimeException()); + return; + } + ImageWriter writer = writers.next(); + + // Prepare ImageWriteParam and IIOMetadata + ImageWriteParam writeParam = writer.getDefaultWriteParam(); + ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); + IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); + String nativeFormat = metadata.getNativeMetadataFormatName(); + + // Create the native metadata tree + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); + IIOMetadataNode textNode = null; + + + // Check if a tEXt node already exists + for (int i = 0; i < root.getChildNodes().getLength(); i++) { + if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { + textNode = (IIOMetadataNode) root.getChildNodes().item(i); + break; + } + } + + // If not, create a new one and append it to the root + if (textNode == null) { + textNode = new IIOMetadataNode("tEXt"); + root.appendChild(textNode); + } + + IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); + textEntry.setAttribute("keyword", "chara"); + textEntry.setAttribute("value", Base64.getEncoder().encodeToString(CharacterUtil.toJson(character).toString().getBytes(StandardCharsets.UTF_8))); + textNode.appendChild(textEntry); + + try { + metadata.setFromTree(nativeFormat, root); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { + writer.setOutput(stream); + writer.write(new IIOImage(image, null, metadata)); + } finally { + writer.dispose(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); +// textEntry.setAttribute("keyword", key); // The metadata key (e.g., Author, Comment) +// textEntry.setAttribute("value", value); // The metadata value +// textNode.appendChild(textEntry); +// +// metadata.setFromTree(nativeFormat, root); +// +// try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { +// writer.setOutput(stream); +// writer.write(new IIOImage(image, null, metadata)); +// } finally { +// writer.dispose(); +// } + + } + + +} diff --git a/src/main/java/me/piitex/app/utils/CharacterCardImporter.java b/src/main/java/me/piitex/app/utils/CharacterCardImporter.java index 909f4303..ae859108 100644 --- a/src/main/java/me/piitex/app/utils/CharacterCardImporter.java +++ b/src/main/java/me/piitex/app/utils/CharacterCardImporter.java @@ -201,6 +201,22 @@ public static String getChatScenario(JSONObject metaData) throws JSONException { return ""; } + public static String getUserDisplay(JSONObject metaData) { + JSONObject characterJson = getCharacterJson(metaData); + if (characterJson.has("userDisplay")) { + return characterJson.getString("userDisplay"); + } + return null; + } + + public static String getUserPersona(JSONObject metaData) { + JSONObject characterJson = getCharacterJson(metaData); + if (characterJson.has("userPersona")) { + return characterJson.getString("userPersona"); + } + return null; + } + public static Map getLoreItems(JSONObject metaData) throws JSONException { JSONObject characterJson = getCharacterJson(metaData); Map map = new HashMap<>(); diff --git a/src/main/java/me/piitex/app/utils/CharacterUtil.java b/src/main/java/me/piitex/app/utils/CharacterUtil.java new file mode 100644 index 00000000..c7d2469a --- /dev/null +++ b/src/main/java/me/piitex/app/utils/CharacterUtil.java @@ -0,0 +1,39 @@ +package me.piitex.app.utils; + +import me.piitex.app.App; +import me.piitex.app.backend.Character; +import org.json.JSONArray; +import org.json.JSONObject; + +public class CharacterUtil { + + public static JSONObject toJson(Character character) { + JSONObject root = new JSONObject(); + + JSONObject characterRoot = new JSONObject(); + characterRoot.put("basePrompt", App.getInstance().getSettings().getGlobalModel().getSettings().getModelInstructions()); + characterRoot.put("customDialogue", ""); + characterRoot.put("firstMessage", character.getFirstMessage()); + characterRoot.put("scenario", character.getChatScenario()); + characterRoot.put("aiDisplayName", character.getDisplayName()); + characterRoot.put("aiName", character.getId()); + characterRoot.put("aiPersona", character.getPersona()); + characterRoot.put("userDisplay", character.getUser().getDisplayName()); + characterRoot.put("userPersona", character.getUser().getPersona()); + + JSONArray loreRoot = new JSONArray(); + + for (String loreId : character.getLorebook().keySet()) { + String loreValue = character.getLorebook().get(loreId); + JSONObject loreEntry = new JSONObject(); + loreEntry.put("id", loreId); + loreEntry.put("key", loreId); + loreEntry.put("value", loreValue); + loreRoot.put(loreEntry); + } + characterRoot.put("loreItems", loreRoot); + root.put("character", characterRoot); + + return root; + } +} diff --git a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java index 30f49c99..eaeac4b3 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java @@ -12,6 +12,7 @@ import me.piitex.app.App; import me.piitex.app.backend.User; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.CharacterCardExporter; import me.piitex.app.utils.CharacterCardImporter; import me.piitex.app.views.characters.CharacterEditView; import me.piitex.engine.containers.CardContainer; @@ -86,6 +87,16 @@ private void buildCharacterTabContent(@Nullable Character character, boolean dup rootLayout.addElement(charDescription); + ButtonOverlay export = new ButtonBuilder("export").setText("Export Character").build(); + export.addStyle(Styles.BUTTON_OUTLINED); + export.onClick(event -> { + FileChooser chooser = new FileChooser(); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); + File output = chooser.showSaveDialog(App.window.getStage()); + new CharacterCardExporter(output, character); + }); + rootLayout.addElement(export); + //rootLayout.addElement(buildExampleDialogue()); this.addElement(parentView.buildSubmitBox()); @@ -194,6 +205,15 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea parentView.getChatTabInstance().getFirstMessageInput().setCurrentText(CharacterCardImporter.getFirstMessage(metadata)); parentView.getChatTabInstance().getChatScenarioInput().setCurrentText(CharacterCardImporter.getChatScenario(metadata)); + String userDisplay = CharacterCardImporter.getUserDisplay(metadata); + if (userDisplay != null) { + parentView.getUserTabInstance().getUserDisplayNameInput().setCurrentText(userDisplay); + } + String userPersona = CharacterCardImporter.getUserPersona(metadata); + if (userPersona != null) { + parentView.getUserTabInstance().getUserDescription().setCurrentText(userPersona); + } + parentView.setCharacterIconPath(file); parentView.getInfoFile().set("icon-path", file.getAbsolutePath()); image.setImage(new ImageLoader(file)); From a05aaf17fbee4a0a154a00910489ec35bf95b7e1 Mon Sep 17 00:00:00 2001 From: piitex Date: Tue, 11 Nov 2025 17:30:07 -0600 Subject: [PATCH 10/22] Window scaling parameter will now write to file. --- src/main/java/me/piitex/app/configuration/AppSettings.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/piitex/app/configuration/AppSettings.java b/src/main/java/me/piitex/app/configuration/AppSettings.java index 2376f16c..51779792 100644 --- a/src/main/java/me/piitex/app/configuration/AppSettings.java +++ b/src/main/java/me/piitex/app/configuration/AppSettings.java @@ -152,12 +152,13 @@ public boolean isWindowScaling() { public void setWindowScaling(boolean windowScaling) { this.windowScaling = windowScaling; + infoFile.set("window-scaling", windowScaling); } /* - Utility functions for getting theme coloring. - Needed for RichTextFX components and BBCode - */ + Utility functions for getting theme coloring. + Needed for RichTextFX components and BBCode + */ public Theme getStyleTheme(String name) { if (name.equalsIgnoreCase("primer light")) { return new PrimerLight(); From 6f9772181c85fd5d97a86e2300ed8abd2a8b5e32 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 05:31:58 -0600 Subject: [PATCH 11/22] Added user exporter and refactored classes. --- .../app/utils/CharacterCardExporter.java | 106 ---------- .../me/piitex/app/utils/CharacterUtil.java | 39 ---- .../piitex/app/utils/ImageCardExporter.java | 183 ++++++++++++++++++ .../me/piitex/app/utils/UserCardImporter.java | 94 +++++++++ .../views/characters/tabs/CharacterTab.java | 27 ++- .../app/views/characters/tabs/UserTab.java | 64 +++++- 6 files changed, 356 insertions(+), 157 deletions(-) delete mode 100644 src/main/java/me/piitex/app/utils/CharacterCardExporter.java delete mode 100644 src/main/java/me/piitex/app/utils/CharacterUtil.java create mode 100644 src/main/java/me/piitex/app/utils/ImageCardExporter.java create mode 100644 src/main/java/me/piitex/app/utils/UserCardImporter.java diff --git a/src/main/java/me/piitex/app/utils/CharacterCardExporter.java b/src/main/java/me/piitex/app/utils/CharacterCardExporter.java deleted file mode 100644 index 08f0a0e2..00000000 --- a/src/main/java/me/piitex/app/utils/CharacterCardExporter.java +++ /dev/null @@ -1,106 +0,0 @@ -package me.piitex.app.utils; - -import me.piitex.app.App; -import me.piitex.app.backend.Character; - -import javax.imageio.*; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; -import javax.imageio.stream.ImageOutputStream; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Base64; -import java.util.Iterator; - -public class CharacterCardExporter { - private final Character character; - - public CharacterCardExporter(File output, Character character) { - this.character = character; - // Using swing for now - try { - Files.copy(new File(character.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new RuntimeException(e); - } - - BufferedImage image; - try { - image = ImageIO.read(output); - } catch (IOException e) { - App.logger.error("Could load character export output image.", e); - return; - } - - Iterator writers = ImageIO.getImageWritersByFormatName("png"); - if (!writers.hasNext()) { - App.logger.error("Could not find png writer for exporter.", new RuntimeException()); - return; - } - ImageWriter writer = writers.next(); - - // Prepare ImageWriteParam and IIOMetadata - ImageWriteParam writeParam = writer.getDefaultWriteParam(); - ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); - IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); - String nativeFormat = metadata.getNativeMetadataFormatName(); - - // Create the native metadata tree - IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); - IIOMetadataNode textNode = null; - - - // Check if a tEXt node already exists - for (int i = 0; i < root.getChildNodes().getLength(); i++) { - if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { - textNode = (IIOMetadataNode) root.getChildNodes().item(i); - break; - } - } - - // If not, create a new one and append it to the root - if (textNode == null) { - textNode = new IIOMetadataNode("tEXt"); - root.appendChild(textNode); - } - - IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); - textEntry.setAttribute("keyword", "chara"); - textEntry.setAttribute("value", Base64.getEncoder().encodeToString(CharacterUtil.toJson(character).toString().getBytes(StandardCharsets.UTF_8))); - textNode.appendChild(textEntry); - - try { - metadata.setFromTree(nativeFormat, root); - - try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { - writer.setOutput(stream); - writer.write(new IIOImage(image, null, metadata)); - } finally { - writer.dispose(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - - // IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); -// textEntry.setAttribute("keyword", key); // The metadata key (e.g., Author, Comment) -// textEntry.setAttribute("value", value); // The metadata value -// textNode.appendChild(textEntry); -// -// metadata.setFromTree(nativeFormat, root); -// -// try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { -// writer.setOutput(stream); -// writer.write(new IIOImage(image, null, metadata)); -// } finally { -// writer.dispose(); -// } - - } - - -} diff --git a/src/main/java/me/piitex/app/utils/CharacterUtil.java b/src/main/java/me/piitex/app/utils/CharacterUtil.java deleted file mode 100644 index c7d2469a..00000000 --- a/src/main/java/me/piitex/app/utils/CharacterUtil.java +++ /dev/null @@ -1,39 +0,0 @@ -package me.piitex.app.utils; - -import me.piitex.app.App; -import me.piitex.app.backend.Character; -import org.json.JSONArray; -import org.json.JSONObject; - -public class CharacterUtil { - - public static JSONObject toJson(Character character) { - JSONObject root = new JSONObject(); - - JSONObject characterRoot = new JSONObject(); - characterRoot.put("basePrompt", App.getInstance().getSettings().getGlobalModel().getSettings().getModelInstructions()); - characterRoot.put("customDialogue", ""); - characterRoot.put("firstMessage", character.getFirstMessage()); - characterRoot.put("scenario", character.getChatScenario()); - characterRoot.put("aiDisplayName", character.getDisplayName()); - characterRoot.put("aiName", character.getId()); - characterRoot.put("aiPersona", character.getPersona()); - characterRoot.put("userDisplay", character.getUser().getDisplayName()); - characterRoot.put("userPersona", character.getUser().getPersona()); - - JSONArray loreRoot = new JSONArray(); - - for (String loreId : character.getLorebook().keySet()) { - String loreValue = character.getLorebook().get(loreId); - JSONObject loreEntry = new JSONObject(); - loreEntry.put("id", loreId); - loreEntry.put("key", loreId); - loreEntry.put("value", loreValue); - loreRoot.put(loreEntry); - } - characterRoot.put("loreItems", loreRoot); - root.put("character", characterRoot); - - return root; - } -} diff --git a/src/main/java/me/piitex/app/utils/ImageCardExporter.java b/src/main/java/me/piitex/app/utils/ImageCardExporter.java new file mode 100644 index 00000000..224fa06e --- /dev/null +++ b/src/main/java/me/piitex/app/utils/ImageCardExporter.java @@ -0,0 +1,183 @@ +package me.piitex.app.utils; + +import me.piitex.app.App; +import me.piitex.app.backend.Character; +import me.piitex.app.backend.User; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.Iterator; + +public class ImageCardExporter { + + public static void exportCharacter(Character character, File output) throws IOException { + + Files.copy(new File(character.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + + BufferedImage image = ImageIO.read(output); + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + App.logger.error("Could not find png writer for exporter.", new RuntimeException()); + return; + } + ImageWriter writer = writers.next(); + + // Prepare ImageWriteParam and IIOMetadata + ImageWriteParam writeParam = writer.getDefaultWriteParam(); + ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); + IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); + String nativeFormat = metadata.getNativeMetadataFormatName(); + + // Create the native metadata tree + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); + IIOMetadataNode textNode = null; + + + // Check if a tEXt node already exists + for (int i = 0; i < root.getChildNodes().getLength(); i++) { + if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { + textNode = (IIOMetadataNode) root.getChildNodes().item(i); + break; + } + } + + // If not, create a new one and append it to the root + if (textNode == null) { + textNode = new IIOMetadataNode("tEXt"); + root.appendChild(textNode); + } + + IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); + textEntry.setAttribute("keyword", "chara"); + textEntry.setAttribute("value", Base64.getEncoder().encodeToString(toJson(character).toString().getBytes(StandardCharsets.UTF_8))); + textNode.appendChild(textEntry); + + + metadata.setFromTree(nativeFormat, root); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { + writer.setOutput(stream); + writer.write(new IIOImage(image, null, metadata)); + } finally { + writer.dispose(); + } + } + + public static void exportUser(User user, File output) throws IOException { + + Files.copy(new File(user.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + + BufferedImage image = ImageIO.read(output); + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + App.logger.error("Could not find png writer for exporter.", new RuntimeException()); + return; + } + ImageWriter writer = writers.next(); + + // Prepare ImageWriteParam and IIOMetadata + ImageWriteParam writeParam = writer.getDefaultWriteParam(); + ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); + IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); + String nativeFormat = metadata.getNativeMetadataFormatName(); + + // Create the native metadata tree + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); + IIOMetadataNode textNode = null; + + + // Check if a tEXt node already exists + for (int i = 0; i < root.getChildNodes().getLength(); i++) { + if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { + textNode = (IIOMetadataNode) root.getChildNodes().item(i); + break; + } + } + + // If not, create a new one and append it to the root + if (textNode == null) { + textNode = new IIOMetadataNode("tEXt"); + root.appendChild(textNode); + } + + IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); + textEntry.setAttribute("keyword", "user"); + textEntry.setAttribute("value", Base64.getEncoder().encodeToString(toJson(user).toString().getBytes(StandardCharsets.UTF_8))); + textNode.appendChild(textEntry); + + + metadata.setFromTree(nativeFormat, root); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { + writer.setOutput(stream); + writer.write(new IIOImage(image, null, metadata)); + } finally { + writer.dispose(); + } + } + + public static JSONObject toJson(Character character) { + JSONObject root = new JSONObject(); + + JSONObject characterRoot = new JSONObject(); + characterRoot.put("basePrompt", App.getInstance().getSettings().getGlobalModel().getSettings().getModelInstructions()); + characterRoot.put("customDialogue", ""); + characterRoot.put("firstMessage", character.getFirstMessage()); + characterRoot.put("scenario", character.getChatScenario()); + characterRoot.put("aiDisplayName", character.getDisplayName()); + characterRoot.put("aiName", character.getId()); + characterRoot.put("aiPersona", character.getPersona()); + + JSONArray loreRoot = new JSONArray(); + + for (String loreId : character.getLorebook().keySet()) { + String loreValue = character.getLorebook().get(loreId); + JSONObject loreEntry = new JSONObject(); + loreEntry.put("id", loreId); + loreEntry.put("key", loreId); + loreEntry.put("value", loreValue); + loreRoot.put(loreEntry); + } + characterRoot.put("loreItems", loreRoot); + root.put("character", characterRoot); + + return root; + } + + public static JSONObject toJson(User user) { + JSONObject root = new JSONObject(); + + JSONObject characterRoot = new JSONObject(); + characterRoot.put("userDisplay", user.getDisplayName()); + characterRoot.put("userPersona", user.getPersona()); + + JSONArray loreRoot = new JSONArray(); + + for (String loreId : user.getLorebook().keySet()) { + String loreValue = user.getLorebook().get(loreId); + JSONObject loreEntry = new JSONObject(); + loreEntry.put("id", loreId); + loreEntry.put("key", loreId); + loreEntry.put("value", loreValue); + loreRoot.put(loreEntry); + } + characterRoot.put("loreItems", loreRoot); + root.put("user", characterRoot); + + return root; + } + +} diff --git a/src/main/java/me/piitex/app/utils/UserCardImporter.java b/src/main/java/me/piitex/app/utils/UserCardImporter.java new file mode 100644 index 00000000..1f06ee36 --- /dev/null +++ b/src/main/java/me/piitex/app/utils/UserCardImporter.java @@ -0,0 +1,94 @@ +package me.piitex.app.utils; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; +import me.piitex.app.App; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class UserCardImporter { + public static JSONObject getImageMetaData(File file) throws ImageProcessingException, IOException { + App.logger.info("Gathering card data..."); + Metadata metadata = ImageMetadataReader.readMetadata(file); + JSONObject toReturn = null; + + + for (Directory directory : metadata.getDirectories()) { + for (Tag tag : directory.getTags()) { + // Slightly easier with Silly Tavern. They all start with Chara: for the metadata. + if (tag.getDescription().startsWith("user")) { + // Remove the beginning "chara: " so it follows the json scheme. + String data = tag.getDescription().replace("user: ", ""); + try { + data = new String(Base64.getDecoder().decode(data)); + } catch (IllegalArgumentException ignored) { + // Thrown if the user entry cannot be decoded into base64. + // This happens if there is multiple user entries which is possible. + // Go to next entry to loop. + continue; + } + try { + toReturn = new JSONObject(data); + return toReturn; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + } + return null; + } + + private static JSONObject getUserJson(JSONObject metaData) throws JSONException { + if (metaData.has("user")) { + return metaData.getJSONObject("user"); + } + // Can process other structures + + return metaData; + } + + public static String getUserDisplay(JSONObject metaData) { + JSONObject userJson = getUserJson(metaData); + if (userJson.has("userDisplay")) { + return userJson.getString("userDisplay"); + } + return null; + } + + public static String getUserPersona(JSONObject metaData) { + JSONObject userJson = getUserJson(metaData); + if (userJson.has("userPersona")) { + return userJson.getString("userPersona"); + } + return null; + } + + public static Map getLoreItems(JSONObject metaData) throws JSONException { + JSONObject userJson = getUserJson(metaData); + Map map = new HashMap<>(); + if (userJson.has("loreItems")) { + + JSONArray array = userJson.getJSONArray("loreItems"); + for (int i = 0; i < array.length(); i++) { + JSONObject object = array.getJSONObject(i); + String key = object.getString("key"); + String value = object.getString("value"); + map.put(key, value); + } + + return map; + } + return map; + } +} diff --git a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java index eaeac4b3..c8c335c4 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java @@ -12,7 +12,7 @@ import me.piitex.app.App; import me.piitex.app.backend.User; import me.piitex.app.configuration.AppSettings; -import me.piitex.app.utils.CharacterCardExporter; +import me.piitex.app.utils.ImageCardExporter; import me.piitex.app.utils.CharacterCardImporter; import me.piitex.app.views.characters.CharacterEditView; import me.piitex.engine.containers.CardContainer; @@ -87,15 +87,22 @@ private void buildCharacterTabContent(@Nullable Character character, boolean dup rootLayout.addElement(charDescription); - ButtonOverlay export = new ButtonBuilder("export").setText("Export Character").build(); - export.addStyle(Styles.BUTTON_OUTLINED); - export.onClick(event -> { - FileChooser chooser = new FileChooser(); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); - File output = chooser.showSaveDialog(App.window.getStage()); - new CharacterCardExporter(output, character); - }); - rootLayout.addElement(export); + if (character != null) { + ButtonOverlay export = new ButtonBuilder("export").setText("Export Character").build(); + export.addStyle(Styles.ACCENT); + export.addStyle(Styles.BUTTON_OUTLINED); + export.onClick(event -> { + FileChooser chooser = new FileChooser(); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); + File output = chooser.showSaveDialog(App.window.getStage()); + try { + ImageCardExporter.exportCharacter(character, output); + } catch (IOException e) { + // Prompt the user with an error message + } + }); + rootLayout.addElement(export); + } //rootLayout.addElement(buildExampleDialogue()); diff --git a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java index 669b8e2c..0fe33833 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java @@ -1,11 +1,15 @@ package me.piitex.app.views.characters.tabs; import atlantafx.base.theme.Styles; +import com.drew.imaging.ImageProcessingException; +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.stage.FileChooser; import me.piitex.app.App; import me.piitex.app.backend.User; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.ImageCardExporter; +import me.piitex.app.utils.UserCardImporter; import me.piitex.os.configurations.InfoFile; import me.piitex.app.views.characters.CharacterEditView; import me.piitex.engine.containers.CardContainer; @@ -15,8 +19,10 @@ import me.piitex.engine.layouts.VerticalLayout; import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -78,10 +84,26 @@ private void buildUserTabContent() { userDescription.addStyle(Styles.BG_DEFAULT); userDescription.addStyle(appSettings.getChatTextSize()); userDescription.addStyle(Styles.TEXT_ON_EMPHASIS); - rootLayout.addElement(userDescription); - this.addElement(parentView.buildSubmitBox()); + if (parentView.getUser() != null) { + ButtonOverlay export = new ButtonBuilder("export").setText("Export User").build(); + export.addStyle(Styles.ACCENT); + export.addStyle(Styles.BUTTON_OUTLINED); + export.onClick(event -> { + FileChooser chooser = new FileChooser(); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); + File output = chooser.showSaveDialog(App.window.getStage()); + try { + ImageCardExporter.exportUser(parentView.getUser(), output); + } catch (IOException e) { + // Prompt the user with an error message + } + }); + rootLayout.addElement(export); + } + + addElement(parentView.buildSubmitBox()); } private CardContainer buildUserDisplay() { @@ -191,6 +213,44 @@ private VerticalLayout buildUserInput() { parentView.updateInfoData(); }); + ButtonOverlay importCard = new ButtonBuilder("import").setText("Import Character Card").build(); + if (parentView.getUser() != null) { + importCard.setEnabled(false); + } + importCard.addStyle(Styles.ACCENT); + importCard.addStyle(Styles.BUTTON_OUTLINED); + importCard.setWidth(200); + importCard.setHeight(50); + + FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); + root.addElement(fileSelector); + fileSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + JSONObject metadata = UserCardImporter.getImageMetaData(file); + userDisplayNameInput.setCurrentText(UserCardImporter.getUserDisplay(metadata)); + userDescription.setCurrentText(UserCardImporter.getUserPersona(metadata)); + + parentView.getLoreBookTabInstance().getItems().clear(); + //TODO: Process user lorebook + + parentView.setUserIconPath(file); + parentView.getInfoFile().set("icon-path-user", file.getAbsolutePath()); + image.setImage(new ImageLoader(file)); + + parentView.updateInfoData(); + + } catch (ImageProcessingException | IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + return root; } From 16fb887840a7e4b22ba532a46bbe986704034339 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 05:32:21 -0600 Subject: [PATCH 12/22] Devices will be outputted to logger. --- src/main/java/me/piitex/app/backend/server/DeviceProcess.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/piitex/app/backend/server/DeviceProcess.java b/src/main/java/me/piitex/app/backend/server/DeviceProcess.java index 1ee3d5c4..b156b3c4 100644 --- a/src/main/java/me/piitex/app/backend/server/DeviceProcess.java +++ b/src/main/java/me/piitex/app/backend/server/DeviceProcess.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.file.Files; import java.util.LinkedList; +import java.util.List; public class DeviceProcess { private final Process process; @@ -37,12 +38,11 @@ public DeviceProcess(String backend) throws IOException { } public void handleOutput() { - App.logger.info("Handling input..."); try { LinkedList lines = new LinkedList<>(Files.readAllLines(new File(App.getDataDirectory(), "devices.txt").toPath())); lines.removeFirst(); App.getInstance().getSettings().setDevices(lines); - + App.logger.info("Devices: {}", List.of(lines)); } catch (IOException e) { throw new RuntimeException(e); } From 4b75e129c65764ee486f6affeb862e98a45cca94 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 07:14:20 -0600 Subject: [PATCH 13/22] Added export button for user templates. --- .../piitex/app/views/users/UserEditView.java | 12 +++++- .../piitex/app/views/users/tabs/UserTab.java | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/piitex/app/views/users/UserEditView.java b/src/main/java/me/piitex/app/views/users/UserEditView.java index 93692352..6abdc5d2 100644 --- a/src/main/java/me/piitex/app/views/users/UserEditView.java +++ b/src/main/java/me/piitex/app/views/users/UserEditView.java @@ -18,7 +18,6 @@ import me.piitex.engine.overlays.ButtonBuilder; import me.piitex.engine.overlays.ButtonOverlay; import me.piitex.engine.overlays.MessageOverlay; -import me.piitex.os.configurations.InfoFile; import java.io.File; import java.io.IOException; @@ -38,6 +37,7 @@ public class UserEditView extends EmptyContainer { private TabsContainer tabsContainer; private UserTab userTab; + private UserLoreBookTab userLoreBookTab; private static final AppSettings appSettings = App.getInstance().getAppSettings(); @@ -76,7 +76,7 @@ public void init() { userTab = new UserTab("User", this); tabsContainer.addTab(userTab); - UserLoreBookTab userLoreBookTab = new UserLoreBookTab("Lorebook", this); + userLoreBookTab = new UserLoreBookTab("Lorebook", this); tabsContainer.addTab(userLoreBookTab); } @@ -171,6 +171,14 @@ public boolean validate() { } } + public UserTab getUserTab() { + return userTab; + } + + public UserLoreBookTab getUserLoreBookTab() { + return userLoreBookTab; + } + public User getUser() { return user; } diff --git a/src/main/java/me/piitex/app/views/users/tabs/UserTab.java b/src/main/java/me/piitex/app/views/users/tabs/UserTab.java index 94322570..1bb5838a 100644 --- a/src/main/java/me/piitex/app/views/users/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/users/tabs/UserTab.java @@ -1,10 +1,13 @@ package me.piitex.app.views.users.tabs; import atlantafx.base.theme.Styles; +import com.drew.imaging.ImageProcessingException; +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.stage.FileChooser; import me.piitex.app.App; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.UserCardImporter; import me.piitex.app.views.users.UserEditView; import me.piitex.engine.containers.CardContainer; import me.piitex.engine.containers.ScrollContainer; @@ -13,8 +16,10 @@ import me.piitex.engine.layouts.VerticalLayout; import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; public class UserTab extends Tab { private final UserEditView userEditView; @@ -151,6 +156,41 @@ private VerticalLayout buildUserInput() { }); root.addElement(userDisplayNameInput); + ButtonOverlay importCard = new ButtonBuilder("import").setText("Import Character Card").build(); + if (userEditView.getUser() != null) { + importCard.setEnabled(false); + } + importCard.addStyle(Styles.ACCENT); + importCard.addStyle(Styles.BUTTON_OUTLINED); + importCard.setWidth(200); + importCard.setHeight(50); + + FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); + root.addElement(fileSelector); + fileSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + JSONObject metadata = UserCardImporter.getImageMetaData(file); + userDisplayNameInput.setCurrentText(UserCardImporter.getUserDisplay(metadata)); + userDescription.setCurrentText(UserCardImporter.getUserPersona(metadata)); + + userEditView.getLoreBook().clear(); + userEditView.getLoreBook().putAll(UserCardImporter.getLoreItems(metadata)); + userEditView.getUserLoreBookTab().buildLorebookTabContent(); + + userEditView.setUserIconPath(file); + image.setImage(new ImageLoader(file)); + } catch (ImageProcessingException | IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + return root; } From a6da88580fe5fd42cbbcc1668da7c19cbf5b67a7 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 07:28:13 -0600 Subject: [PATCH 14/22] Fixed design layout with user templates. --- src/main/java/me/piitex/app/views/users/UserEditView.java | 2 ++ src/main/java/me/piitex/app/views/users/UsersView.java | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/me/piitex/app/views/users/UserEditView.java b/src/main/java/me/piitex/app/views/users/UserEditView.java index 6abdc5d2..62e805e1 100644 --- a/src/main/java/me/piitex/app/views/users/UserEditView.java +++ b/src/main/java/me/piitex/app/views/users/UserEditView.java @@ -59,6 +59,8 @@ public UserEditView(User user) { } public void init() { + addStyle(Styles.BG_INSET); + HorizontalLayout root = new HorizontalLayout(getWidth(), getHeight()); root.setMaxSize(root.getWidth(), root.getHeight()); addElement(root); diff --git a/src/main/java/me/piitex/app/views/users/UsersView.java b/src/main/java/me/piitex/app/views/users/UsersView.java index c9e79dae..afdd568d 100644 --- a/src/main/java/me/piitex/app/views/users/UsersView.java +++ b/src/main/java/me/piitex/app/views/users/UsersView.java @@ -2,7 +2,6 @@ import atlantafx.base.theme.Styles; import javafx.application.Platform; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ContextMenu; @@ -34,7 +33,7 @@ public class UsersView extends EmptyContainer { public UsersView() { super(appSettings.getWidth(), appSettings.getHeight()); - + addStyle(Styles.BG_INSET); HorizontalLayout root = new HorizontalLayout(getWidth(), getHeight()); root.setMaxSize(root.getWidth(), root.getHeight()); addElement(root); @@ -50,7 +49,8 @@ public UsersView() { } public void init() { - VerticalLayout header = new VerticalLayout(appSettings.getWidth() - 200, 200); + VerticalLayout header = new VerticalLayout(appSettings.getWidth() - 200, 100); + header.addStyle(Styles.BG_DEFAULT); header.setMaxSize(header.getWidth(), header.getHeight()); header.setAlignment(Pos.CENTER); mainPage.addElement(header); @@ -63,7 +63,6 @@ public void init() { App.window.addContainer(new UserEditView()); }); header.addElement(newUser); - header.addElement(new SeparatorOverlay(Orientation.HORIZONTAL)); buildFlowLayout(); } @@ -75,6 +74,7 @@ public void buildFlowLayout() { base.setMaxSize(base.getWidth(), base.getHeight()); FlowLayout flowLayout = new FlowLayout(appSettings.getWidth() - 200, 0); + flowLayout.setX(10); flowLayout.setMaxSize(flowLayout.getWidth(), flowLayout.getHeight()); mainPage.addElement(flowLayout); flowLayout.addStyle(Styles.BORDER_DEFAULT); From b6f67217aba304891b52366ee9a4726fd97e9d20 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 08:44:50 -0600 Subject: [PATCH 15/22] Fixed issues when changing text size. --- .../me/piitex/app/views/settings/SettingsView.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/me/piitex/app/views/settings/SettingsView.java b/src/main/java/me/piitex/app/views/settings/SettingsView.java index f03ba9de..cebefb09 100644 --- a/src/main/java/me/piitex/app/views/settings/SettingsView.java +++ b/src/main/java/me/piitex/app/views/settings/SettingsView.java @@ -167,6 +167,11 @@ public TileContainer buildChatSize() { item = Styles.TEXT; } appSettings.setChatTextSize(item); + + App.getInstance().getCharacters().values().forEach(character -> character.getChatViewCachedNodes().clear()); + App.window.clear(); + App.window.close(false); + App.getInstance().initialization(App.window.getStage()); }); tileContainer.setAction(selection); @@ -218,11 +223,10 @@ public TileContainer buildGlobalChatSize() { appSettings.setGlobalTextSize(item); // Refresh view to reflect changes. - container.getElements().clear(); - build(); - Pane pane = (Pane) container.getNode(); - pane.getChildren().clear(); - pane.getChildren().addAll(container.build()); + App.getInstance().getCharacters().values().forEach(character -> character.getChatViewCachedNodes().clear()); + App.window.clear(); + App.window.close(false); + App.getInstance().initialization(App.window.getStage()); }); From eee6f2f064b460f01f403a7ebee2f0c7cb1d93fc Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 14:40:13 -0600 Subject: [PATCH 16/22] Fixed issues for detecting running process. --- src/main/java/me/piitex/app/App.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/piitex/app/App.java b/src/main/java/me/piitex/app/App.java index 4316a975..73cad8fa 100644 --- a/src/main/java/me/piitex/app/App.java +++ b/src/main/java/me/piitex/app/App.java @@ -74,12 +74,13 @@ public void preInitialization() { setupDirectories(); settings = new ServerSettings(); + appSettings = new AppSettings(); long currentPid = ProcessHandle.current().pid(); if (settings.getInfoFile().hasKey("main-pid")) { String pid = settings.getInfoFile().get("main-pid"); if (ProcessUtil.isProcessRunning(Long.parseLong(pid))) { - logger.error("Process already running!"); + logger.error("Process already running! '{}'", pid); error = true; Platform.runLater(() -> { buildErrorWindow("Process is already running!").render(); @@ -101,7 +102,6 @@ public void preInitialization() { App.logger.info("Finished pre-initialization."); loading = false; }); - appSettings = new AppSettings(); } @Override @@ -326,12 +326,22 @@ public void performUpdates() { downloader.startDownload(dataFileUrl, currentData); } - logger.info("Model list updated..."); + logger.info("Model list updated."); downloader.shutdown(); } catch (IOException e) { App.logger.error("Failed to fetch download size."); } +// TODO: Automatically update llamacpp +// try { +// GitHubUtil gitHubUtil = new GitHubUtil("https://api.github.com/repos/ggerganov/llama.cpp/"); +// FileDownloader llamaDownloader = gitHubUtil.downloadAsset(gitHubUtil.getReleaseAsset(gitHubUtil.getLatestReleaseID(), "llama-[a-zA-Z0-9]+-bin-win-cuda-12\\.4-x64\\.zip").getInt("id"), new File("output/download.zip")); +// +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + + } public AppSettings getAppSettings() { @@ -400,6 +410,8 @@ public Window buildErrorWindow(String message) { App.logger.info("Forcefully killing old process."); ProcessUtil.terminateProcess(Long.parseLong(settings.getInfoFile().get("main-pid"))); } + + appSettings.getInfoFile().set("main-pid", ""); }); window.getStage().setOnCloseRequest(event -> { From 5d13af8a994053c06e944e2bba033ccb6993fafe Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 14:51:23 -0600 Subject: [PATCH 17/22] Updated last-chat field when switching between chats. --- src/main/java/me/piitex/app/views/chats/ChatView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/me/piitex/app/views/chats/ChatView.java b/src/main/java/me/piitex/app/views/chats/ChatView.java index b54fec0c..68fe0e9d 100644 --- a/src/main/java/me/piitex/app/views/chats/ChatView.java +++ b/src/main/java/me/piitex/app/views/chats/ChatView.java @@ -207,6 +207,7 @@ public ChoiceBoxOverlay buildSelection() { App.window.clearContainers(); App.window.addContainer(new ChatView(character, next, true)); } + character.setLastChat(next); }); return selection; From 5831d0d8be3c0b20c30a1296b809b060c51bd71b Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 15:03:10 -0600 Subject: [PATCH 18/22] Fixed incorrect output path for user templates. --- .../me/piitex/app/views/characters/CharacterEditView.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/piitex/app/views/characters/CharacterEditView.java b/src/main/java/me/piitex/app/views/characters/CharacterEditView.java index 5544bd78..7b24d46e 100644 --- a/src/main/java/me/piitex/app/views/characters/CharacterEditView.java +++ b/src/main/java/me/piitex/app/views/characters/CharacterEditView.java @@ -340,9 +340,11 @@ public HorizontalLayout buildSubmitBox() { characterSpecificUser.setPersona(userPersona); if (userIconPath != null && userIconPath.exists()) { - File output = new File(currentCharacterInstance.getUserDirectory(), userIconPath.getName()); - Files.copy(userIconPath.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - characterSpecificUser.setIconPath(output.getAbsolutePath()); + File output = new File(character.getUserDirectory(), userIconPath.getName()); + if (!userIconPath.toPath().equals(output.toPath())) { + Files.copy(userIconPath.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + characterSpecificUser.setIconPath(output.getAbsolutePath()); + } } character.setUser(characterSpecificUser); From 296d38f1a6012ea0c2b3d0a385b3a480bd597c82 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 15:17:36 -0600 Subject: [PATCH 19/22] Implemented user template lorebook. --- .../piitex/app/views/characters/tabs/UserTab.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java index 0fe33833..6709cae4 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java @@ -208,6 +208,13 @@ private VerticalLayout buildUserInput() { parentView.setUserIconPath(new File(template.getIconPath())); image.setImage(new ImageLoader(parentView.getUserIconPath())); } + + if (parentView.getUser() != null) { + parentView.getUser().getLorebook().keySet().forEach(s -> parentView.getLoreBookTabInstance().getItems().remove(s)); + } + + parentView.getLoreBookTabInstance().getItems().putAll(template.getLorebook()); + parentView.getLoreBookTabInstance().buildLorebookTabContent(); } parentView.updateInfoData(); @@ -231,8 +238,11 @@ private VerticalLayout buildUserInput() { userDisplayNameInput.setCurrentText(UserCardImporter.getUserDisplay(metadata)); userDescription.setCurrentText(UserCardImporter.getUserPersona(metadata)); - parentView.getLoreBookTabInstance().getItems().clear(); - //TODO: Process user lorebook + if (parentView.getUser() != null) { + parentView.getUser().getLorebook().keySet().forEach(s -> parentView.getLoreBookTabInstance().getItems().remove(s)); + } + parentView.getLoreBookTabInstance().getItems().putAll(UserCardImporter.getLoreItems(metadata)); + parentView.getLoreBookTabInstance().buildLorebookTabContent(); parentView.setUserIconPath(file); parentView.getInfoFile().set("icon-path-user", file.getAbsolutePath()); From 6885633b4ac1f064e9ce911ca1cfed4b02ccefd3 Mon Sep 17 00:00:00 2001 From: piitex Date: Wed, 12 Nov 2025 15:19:03 -0600 Subject: [PATCH 20/22] Fixed import button name and removed disabling of the button. --- .../java/me/piitex/app/views/characters/tabs/UserTab.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java index 6709cae4..a27f963d 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java @@ -220,10 +220,7 @@ private VerticalLayout buildUserInput() { parentView.updateInfoData(); }); - ButtonOverlay importCard = new ButtonBuilder("import").setText("Import Character Card").build(); - if (parentView.getUser() != null) { - importCard.setEnabled(false); - } + ButtonOverlay importCard = new ButtonBuilder("import").setText("Import User Card").build(); importCard.addStyle(Styles.ACCENT); importCard.addStyle(Styles.BUTTON_OUTLINED); importCard.setWidth(200); From 209ef15d448a7b25694a1a3f0679b6723c31eaed Mon Sep 17 00:00:00 2001 From: piitex Date: Thu, 13 Nov 2025 06:21:33 -0600 Subject: [PATCH 21/22] Fixed issue where character image would be deleted. --- .../java/me/piitex/app/views/characters/CharactersView.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/piitex/app/views/characters/CharactersView.java b/src/main/java/me/piitex/app/views/characters/CharactersView.java index c8635f93..951c5764 100644 --- a/src/main/java/me/piitex/app/views/characters/CharactersView.java +++ b/src/main/java/me/piitex/app/views/characters/CharactersView.java @@ -19,6 +19,7 @@ import me.piitex.engine.layouts.FlowLayout; import me.piitex.engine.layouts.HorizontalLayout; import me.piitex.engine.layouts.VerticalLayout; +import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; import org.apache.commons.io.FileUtils; import org.kordamp.ikonli.javafx.FontIcon; @@ -255,7 +256,9 @@ private void deleteCharacter(FlowLayout base, CardContainer card, Character char // Add a buffer to ensure image resources are disposed. App.getThreadPoolManager().submitSchedule(() -> { try { - App.logger.info("Deleting: {}", character.getId()); + App.logger.info("Removing image from cache '{}'", character.getIconPath()); + ImageLoader.imageCache.remove(character.getIconPath()); // Clear image from cache. + App.logger.info("Deleting Character: {}", character.getId()); FileUtils.deleteDirectory(character.getCharacterDirectory()); } catch (IOException e) { App.logger.error("Could not delete directory!", e); From 0fe53ebdb1ec3e2fe3a04b014efb1f5a370535e6 Mon Sep 17 00:00:00 2001 From: piitex Date: Thu, 13 Nov 2025 06:22:04 -0600 Subject: [PATCH 22/22] Moved export button under import button. --- .../views/characters/tabs/CharacterTab.java | 54 +++++++++++-------- .../app/views/characters/tabs/UserTab.java | 50 ++++++++++------- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java index c8c335c4..71dd3744 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java @@ -86,24 +86,6 @@ private void buildCharacterTabContent(@Nullable Character character, boolean dup charDescription.addStyle(Styles.TEXT_ON_EMPHASIS); rootLayout.addElement(charDescription); - - if (character != null) { - ButtonOverlay export = new ButtonBuilder("export").setText("Export Character").build(); - export.addStyle(Styles.ACCENT); - export.addStyle(Styles.BUTTON_OUTLINED); - export.onClick(event -> { - FileChooser chooser = new FileChooser(); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); - File output = chooser.showSaveDialog(App.window.getStage()); - try { - ImageCardExporter.exportCharacter(character, output); - } catch (IOException e) { - // Prompt the user with an error message - } - }); - rootLayout.addElement(export); - } - //rootLayout.addElement(buildExampleDialogue()); this.addElement(parentView.buildSubmitBox()); @@ -195,9 +177,11 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea importCard.setWidth(200); importCard.setHeight(50); - FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); - root.addElement(fileSelector); - fileSelector.onFileSelect(event -> { + FileChooserOverlay importSelector = new FileChooserOverlay(App.window, importCard); + importSelector.setText("Import character card."); + importSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(importSelector); + importSelector.onFileSelect(event -> { File file = event.getDirectory(); try { JSONObject metadata = CharacterCardImporter.getImageMetaData(file); @@ -238,6 +222,34 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea } }); + ButtonOverlay exportCard = new ButtonBuilder("import").setText("Export Character Card").build(); + if (character == null) { + exportCard.setEnabled(false); + } + exportCard.addStyle(Styles.ACCENT); + exportCard.addStyle(Styles.BUTTON_OUTLINED); + exportCard.setWidth(200); + exportCard.setHeight(50); + + FileChooserOverlay exportSelector = new FileChooserOverlay(App.window, exportCard); + exportSelector.setText("Export character card as."); + exportSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(exportSelector); + exportSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + // Character cannot be null as the button is disabled if it is. + ImageCardExporter.exportCharacter(character, file); + } catch (IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); return root; } diff --git a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java index a27f963d..880eff1e 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java @@ -86,23 +86,6 @@ private void buildUserTabContent() { userDescription.addStyle(Styles.TEXT_ON_EMPHASIS); rootLayout.addElement(userDescription); - if (parentView.getUser() != null) { - ButtonOverlay export = new ButtonBuilder("export").setText("Export User").build(); - export.addStyle(Styles.ACCENT); - export.addStyle(Styles.BUTTON_OUTLINED); - export.onClick(event -> { - FileChooser chooser = new FileChooser(); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Save exported image as.", "*.png")); - File output = chooser.showSaveDialog(App.window.getStage()); - try { - ImageCardExporter.exportUser(parentView.getUser(), output); - } catch (IOException e) { - // Prompt the user with an error message - } - }); - rootLayout.addElement(export); - } - addElement(parentView.buildSubmitBox()); } @@ -227,6 +210,8 @@ private VerticalLayout buildUserInput() { importCard.setHeight(50); FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); + fileSelector.setText("Import user card."); + fileSelector.setFileExtensions(new String[]{"*.png"}); root.addElement(fileSelector); fileSelector.onFileSelect(event -> { File file = event.getDirectory(); @@ -248,9 +233,38 @@ private VerticalLayout buildUserInput() { parentView.updateInfoData(); } catch (ImageProcessingException | IOException e) { + App.logger.error("Error importing user card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import user card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + + ButtonOverlay exportCard = new ButtonBuilder("export").setText("Export User Card").build(); + if (parentView.getUser() == null) { + exportCard.setEnabled(false); + } + exportCard.addStyle(Styles.ACCENT); + exportCard.addStyle(Styles.BUTTON_OUTLINED); + exportCard.setWidth(200); + exportCard.setHeight(50); + + FileChooserOverlay exportSelector = new FileChooserOverlay(App.window, exportCard); + exportSelector.setText("Export user card as."); + exportSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(exportSelector); + exportSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + // User cannot be null as the button is disabled if it is. + ImageCardExporter.exportUser(parentView.getUser(), file); + } catch (IOException e) { App.logger.error("Error importing character card: ", e); Platform.runLater(() -> { - MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import user card: " + e.getMessage()); errorOverlay.addStyle(Styles.DANGER); errorOverlay.addStyle(Styles.BG_DEFAULT); App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null);