diff --git a/config.properties b/config.properties index 31f303a..7f623ff 100644 --- a/config.properties +++ b/config.properties @@ -1,5 +1,5 @@ #Llama Runner Configuration -#Sun Mar 29 17:31:07 CEST 2026 +#Sun Mar 29 17:54:05 CEST 2026 windowHeight=1189 windowWidth=711 windowX=1849 diff --git a/src/main/java/cz/kamma/llamarunner/ConfigValidation.java b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java index 9d29290..4d75b95 100644 --- a/src/main/java/cz/kamma/llamarunner/ConfigValidation.java +++ b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java @@ -10,112 +10,73 @@ import com.google.gson.JsonSyntaxException; */ public class ConfigValidation { - private static final Gson GSON = new Gson(); - /** - * Validates numeric fields for valid ranges. + * Validates a ModelConfig object. * - * @return a list of error messages (empty if valid) + * @param config the model config to validate + * @return error message if invalid, null if valid */ - public static java.util.List validateNumericInputs( - String host, - int port, - int parallel, - int threads, - double temperature, - double topP, - int topK, - double minP, - int ctxSize, - int ngl) { - - java.util.List errors = new java.util.ArrayList<>(); - + public static String validateConfig(ModelConfig config) { // Validate port - if (port < 1 || port > 65535) { - errors.add("Port must be between 1 and 65535"); + if (config.getPort() < 1 || config.getPort() > 65535) { + return "Port must be between 1 and 65535"; } // Validate parallel - if (parallel < 1) { - errors.add("Parallel must be at least 1"); + if (config.getParallel() < 1) { + return "Parallel must be at least 1"; } // Validate threads - if (threads < 1) { - errors.add("Threads must be at least 1"); + if (config.getThreads() < 1) { + return "Threads must be at least 1"; } // Validate temperature - if (temperature < 0) { - errors.add("Temperature must be non-negative"); + if (config.getTemperature() < 0) { + return "Temperature must be non-negative"; } // Validate topP - if (topP < 0 || topP > 1) { - errors.add("Top P must be between 0 and 1"); + if (config.getTopP() < 0 || config.getTopP() > 1) { + return "Top P must be between 0 and 1"; } // Validate topK - if (topK < 0) { - errors.add("Top K must be non-negative"); + if (config.getTopK() < 0) { + return "Top K must be non-negative"; } // Validate minP - if (minP < 0 || minP > 1) { - errors.add("Min P must be between 0 and 1"); + if (config.getMinP() < 0 || config.getMinP() > 1) { + return "Min P must be between 0 and 1"; } // Validate ctxSize - if (ctxSize < 0) { - errors.add("Context size must be non-negative"); + if (config.getCtxSize() < 0) { + return "Context size must be non-negative"; } - // Validate ngl - if (ngl < 0) { - errors.add("GPU layers must be non-negative"); - } - - return errors; - } - - /** - * Validates JSON string for chatTemplateKwargs. - * - * @param json the JSON string to validate - * @return error message if invalid, null if valid - */ - public static String validateJsonKwargs(String json) { - if (json == null || json.trim().isEmpty()) { - return null; - } - - try { - GSON.fromJson(json, Object.class); - return null; - } catch (JsonSyntaxException e) { - return "Invalid JSON format in kwargs"; - } - } - - /** - * Validates model path exists. - * - * @param modelPath the model file path - * @return error message if file doesn't exist, null if valid - */ - public static String validateModelPath(String modelPath) { - if (modelPath == null || modelPath.trim().isEmpty()) { + // Validate model path + if (config.getModelPath() == null || config.getModelPath().trim().isEmpty()) { return "Model path cannot be empty"; } - - File modelFile = new File(modelPath); + File modelFile = new File(config.getModelPath()); if (!modelFile.exists()) { - return "Model file does not exist: " + modelPath; + return "Model file does not exist: " + config.getModelPath(); + } + if (!modelFile.isFile()) { + return "Model path is not a file: " + config.getModelPath(); } - if (!modelFile.isFile()) { - return "Model path is not a file: " + modelPath; + // Validate JSON kwargs + String json = config.getChatTemplateKwargs(); + if (json != null && !json.trim().isEmpty()) { + try { + new Gson().fromJson(json, Object.class); + } catch (JsonSyntaxException e) { + return "Invalid JSON format in kwargs"; + } } return null; diff --git a/src/main/java/cz/kamma/llamarunner/InputDialog.java b/src/main/java/cz/kamma/llamarunner/InputDialog.java new file mode 100644 index 0000000..a2aea32 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/InputDialog.java @@ -0,0 +1,95 @@ +package cz.kamma.llamarunner; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * Reusable dialog for simple text input. + */ +public class InputDialog { + + private final JDialog dialog; + private final String[] result = new String[1]; + + /** + * Shows a dialog for text input. + * + * @param parent the parent frame + * @param title the dialog title + * @param label the label text + * @param defaultValue the default value + * @return the input text, or null if cancelled + */ + public static String showInputDialog(java.awt.Frame parent, String title, String label, String defaultValue) { + InputDialog dialog = new InputDialog(parent, title, label, defaultValue); + dialog.show(); + return dialog.getResult(); + } + + private InputDialog(java.awt.Frame parent, String title, String label, String defaultValue) { + dialog = new JDialog(parent, title, true); + dialog.setResizable(false); + + // Input panel + JPanel inputPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(4, 4, 4, 4); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + + gbc.gridx = 0; + gbc.gridy = 0; + inputPanel.add(new JLabel(label), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + JTextField textField = new JTextField(20); + textField.setText(defaultValue); + textField.setCaretColor(Color.WHITE); + inputPanel.add(textField, gbc); + + dialog.add(inputPanel, BorderLayout.CENTER); + + // Button panel + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> { + result[0] = textField.getText().trim(); + dialog.dispose(); + }); + buttonPanel.add(okButton); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> { + result[0] = null; + dialog.dispose(); + }); + buttonPanel.add(cancelButton); + + dialog.add(buttonPanel, BorderLayout.SOUTH); + + // Center dialog + dialog.setSize(350, 100); + dialog.setLocationRelativeTo(parent); + } + + private void show() { + dialog.setVisible(true); + } + + private String getResult() { + if (result[0] == null) { + return null; + } + return result[0].isEmpty() ? null : result[0]; + } +} diff --git a/src/main/java/cz/kamma/llamarunner/Main.java b/src/main/java/cz/kamma/llamarunner/Main.java index 920b5a2..5adf54d 100644 --- a/src/main/java/cz/kamma/llamarunner/Main.java +++ b/src/main/java/cz/kamma/llamarunner/Main.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.List; import javax.swing.BorderFactory; -import javax.swing.JDialog; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; @@ -313,16 +312,27 @@ public class Main extends JFrame { } try { + String validationError = validateCurrentConfig(); + if (validationError != null) { + JOptionPane.showMessageDialog(this, validationError, "Validation Error", + JOptionPane.ERROR_MESSAGE); + return; + } saveProfileToName(currentProfile); JOptionPane.showMessageDialog(this, "Profile saved!"); } catch (IOException e) { e.printStackTrace(); - JOptionPane.showMessageDialog(this, "Error saving: " + e.getMessage() + "\n" + e.toString(), "Error", + JOptionPane.showMessageDialog(this, "Error saving: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } + private String validateCurrentConfig() { + ModelConfig config = buildModelConfig(); + return ConfigValidation.validateConfig(config); + } + private void saveProfileToName(String name) throws IOException { ModelConfig config = buildModelConfig(); profileManager.saveProfile(name, config); @@ -404,58 +414,9 @@ public class Main extends JFrame { String modelName = (String) modelComboBox.getSelectedItem(); String defaultName = modelName != null ? modelName : ""; - String[] result = new String[1]; + String newName = InputDialog.showInputDialog(this, "Save profile as...", "Profile name:", defaultName); - JDialog dialog = new JDialog(this, "Save profile as...", true); - dialog.setLayout(new BorderLayout(8, 8)); - dialog.setResizable(false); - - // Input panel - JPanel inputPanel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(4, 4, 4, 4); - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - - gbc.gridx = 0; - gbc.gridy = 0; - inputPanel.add(new JLabel("Profile name:"), gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - JTextField nameField = new JTextField(20); - nameField.setText(defaultName); - nameField.setCaretColor(Color.WHITE); - inputPanel.add(nameField, gbc); - - dialog.add(inputPanel, BorderLayout.CENTER); - - // Button panel - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); - JButton okButton = new JButton("OK"); - okButton.addActionListener(e -> { - result[0] = nameField.getText().trim(); - dialog.dispose(); - }); - buttonPanel.add(okButton); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> { - result[0] = null; - dialog.dispose(); - }); - buttonPanel.add(cancelButton); - - dialog.add(buttonPanel, BorderLayout.SOUTH); - - // Center dialog - dialog.setSize(350, 100); - dialog.setLocationRelativeTo(this); - dialog.setVisible(true); - - String newName = result[0]; - - if (newName == null || newName.isEmpty()) { + if (newName == null) { return; // Cancelled by user } @@ -531,58 +492,9 @@ public class Main extends JFrame { return; } - String[] result = new String[1]; + String newName = InputDialog.showInputDialog(this, "Rename profile", "New profile name:", oldName); - JDialog dialog = new JDialog(this, "Rename profile", true); - dialog.setLayout(new BorderLayout(8, 8)); - dialog.setResizable(false); - - // Input panel - JPanel inputPanel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(4, 4, 4, 4); - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - - gbc.gridx = 0; - gbc.gridy = 0; - inputPanel.add(new JLabel("New profile name:"), gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - JTextField nameField = new JTextField(20); - nameField.setText(oldName); - nameField.setCaretColor(Color.WHITE); - inputPanel.add(nameField, gbc); - - dialog.add(inputPanel, BorderLayout.CENTER); - - // Button panel - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); - JButton okButton = new JButton("OK"); - okButton.addActionListener(e -> { - result[0] = nameField.getText().trim(); - dialog.dispose(); - }); - buttonPanel.add(okButton); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> { - result[0] = null; - dialog.dispose(); - }); - buttonPanel.add(cancelButton); - - dialog.add(buttonPanel, BorderLayout.SOUTH); - - // Center dialog - dialog.setSize(350, 100); - dialog.setLocationRelativeTo(this); - dialog.setVisible(true); - - String newName = result[0]; - - if (newName == null || newName.isEmpty()) { + if (newName == null) { return; // Cancelled by user } @@ -895,15 +807,7 @@ public class Main extends JFrame { ctxSizeField = new JTextField("180000", 10); ctxSizeField.setCaretColor(Color.WHITE); ctxSizeField.addActionListener(e -> updateCommandPreview()); - // Update preview on any change, not just Enter - ctxSizeField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { updateCommandPreview(); } - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { updateCommandPreview(); } - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { updateCommandPreview(); } - }); + ctxSizeField.getDocument().addDocumentListener(new PreviewUpdateListener(this::updateCommandPreview)); panel.add(ctxSizeField, gbc); gbc.gridx = 0; @@ -916,22 +820,7 @@ public class Main extends JFrame { gbc.weightx = 1.0; kwargsField = new JTextField("{\"enable_thinking\": true}"); kwargsField.setCaretColor(Color.WHITE); - kwargsField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - }); + kwargsField.getDocument().addDocumentListener(new PreviewUpdateListener(this::updateCommandPreview)); panel.add(kwargsField, gbc); return panel; @@ -987,22 +876,7 @@ public class Main extends JFrame { } private javax.swing.event.DocumentListener createNglDocumentListener() { - return new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { - updateCommandPreview(); - } - }; + return new PreviewUpdateListener(this::updateCommandPreview); } private void copyCommandToClipboard() { diff --git a/src/main/java/cz/kamma/llamarunner/PreviewUpdateListener.java b/src/main/java/cz/kamma/llamarunner/PreviewUpdateListener.java new file mode 100644 index 0000000..a1a3ba3 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/PreviewUpdateListener.java @@ -0,0 +1,31 @@ +package cz.kamma.llamarunner; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * DocumentListener that updates command preview on text changes. + */ +public class PreviewUpdateListener implements DocumentListener { + + private final Runnable updateCallback; + + public PreviewUpdateListener(Runnable updateCallback) { + this.updateCallback = updateCallback; + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateCallback.run(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateCallback.run(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateCallback.run(); + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ProfileValidator.java b/src/main/java/cz/kamma/llamarunner/ProfileValidator.java index 62014cf..7522526 100644 --- a/src/main/java/cz/kamma/llamarunner/ProfileValidator.java +++ b/src/main/java/cz/kamma/llamarunner/ProfileValidator.java @@ -34,18 +34,4 @@ public class ProfileValidator { return null; } - /** - * Checks if a profile name is unique among existing profiles. - * - * @param newName the new profile name - * @param existingProfiles list of existing profile names - * @return true if unique, false otherwise - */ - public static boolean isProfileNameUnique(String newName, java.util.List existingProfiles) { - if (existingProfiles == null) { - return true; - } - return !existingProfiles.stream() - .anyMatch(name -> name.equalsIgnoreCase(newName)); - } -} + }