commit 04d294c65b31fcd455c0b77efa24176729091615 Author: Radek Davidek Date: Mon Mar 23 14:10:15 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..2c48ba4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +.classpath +.project +.vscode +.claude \ No newline at end of file diff --git a/config.properties b/config.properties new file mode 100644 index 0000000..02448d5 --- /dev/null +++ b/config.properties @@ -0,0 +1,6 @@ +#Llama Runner Configuration +#Mon Mar 23 12:24:12 CET 2026 +windowHeight=854 +windowWidth=587 +windowX=1973 +windowY=546 diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..7686dca --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + cz.kamma + llama-runner + Llama Runner + 1.0-SNAPSHOT + GUI application for running llama-server with customizable parameters + + + + maven-compiler-plugin + 3.11.0 + + 11 + 11 + + + + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + cz.kamma.llamarunner.Main + + + + + + + + + + 11 + 11 + UTF-8 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..577c334 --- /dev/null +++ b/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + cz.kamma + llama-runner + 1.0-SNAPSHOT + jar + + Llama Runner + GUI application for running llama-server with customizable parameters + + + 11 + 11 + UTF-8 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + cz.kamma.llamarunner.Main + + + + + + + + + \ No newline at end of file diff --git a/profiles.json b/profiles.json new file mode 100644 index 0000000..137e5be --- /dev/null +++ b/profiles.json @@ -0,0 +1,97 @@ +{ + "Nemotron Cascade Q8 160k": { + "host": "0.0.0.0", + "port": 3080, + "parallel": 1, + "threads": 99, + "flashAttention": true, + "kvUnified": true, + "cacheTypeK": "q8_0", + "cacheTypeV": "q8_0", + "temperature": 0.6, + "topP": 0.95, + "topK": 20, + "minP": 0.0, + "ctxSize": 160000, + "enableThinking": false, + "modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q8_0.gguf", + "chatTemplateKwargs": "{\"enable_thinking\": false}", + "ngl": -1 + }, + "Qwen3.5-q6k-180k": { + "host": "0.0.0.0", + "port": 3080, + "parallel": 1, + "threads": 99, + "flashAttention": true, + "kvUnified": true, + "cacheTypeK": "q8_0", + "cacheTypeV": "q8_0", + "temperature": 0.6, + "topP": 0.95, + "topK": 20, + "minP": 0.0, + "ctxSize": 180000, + "enableThinking": false, + "modelPath": "/home/kamma/models/Qwen3.5-35B-A3B-Q6_K.gguf", + "chatTemplateKwargs": "{\"enable_thinking\": false}", + "ngl": 999 + }, + "QwenCoderNext-160k": { + "host": "0.0.0.0", + "port": 3080, + "parallel": 1, + "threads": 99, + "flashAttention": true, + "kvUnified": true, + "cacheTypeK": "q8_0", + "cacheTypeV": "q8_0", + "temperature": 0.6, + "topP": 0.95, + "topK": 20, + "minP": 0.0, + "ctxSize": 160000, + "enableThinking": false, + "modelPath": "/home/kamma/models/Qwen3-Coder-Next-UD-Q2_K_XL.gguf", + "chatTemplateKwargs": "{\"enable_thinking\": false}", + "ngl": 999 + }, + "Nemotron Cascade 180k": { + "host": "0.0.0.0", + "port": 3080, + "parallel": 1, + "threads": 99, + "flashAttention": true, + "kvUnified": true, + "cacheTypeK": "q8_0", + "cacheTypeV": "q8_0", + "temperature": 0.6, + "topP": 0.95, + "topK": 20, + "minP": 0.0, + "ctxSize": 180000, + "enableThinking": false, + "modelPath": "/home/kamma/models/Nemotron-Cascade-2-30B-A3B.Q5_K_M.gguf", + "chatTemplateKwargs": "{\"enable_thinking\": false}", + "ngl": 999 + }, + "Qwen3.5 q6xl 160k": { + "host": "0.0.0.0", + "port": 3080, + "parallel": 1, + "threads": 99, + "flashAttention": true, + "kvUnified": true, + "cacheTypeK": "q8_0", + "cacheTypeV": "q8_0", + "temperature": 0.6, + "topP": 0.95, + "topK": 20, + "minP": 0.0, + "ctxSize": 160000, + "enableThinking": false, + "modelPath": "/home/kamma/models/Qwen3.5-35B-A3B-UD-Q6_K_XL.gguf", + "chatTemplateKwargs": "{\"enable_thinking\": false}", + "ngl": -1 + } +} \ No newline at end of file diff --git a/src/main/java/cz/kamma/llamarunner/AppConfig.java b/src/main/java/cz/kamma/llamarunner/AppConfig.java new file mode 100644 index 0000000..1318050 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/AppConfig.java @@ -0,0 +1,102 @@ +package cz.kamma.llamarunner; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +/** + * Application configuration management for storing window size and position. + */ +public class AppConfig { + + private static final int DEFAULT_WIDTH = 900; + private static final int DEFAULT_HEIGHT = 750; + private static final int DEFAULT_X = -1; // -1 means center on screen + private static final int DEFAULT_Y = -1; + + private int windowWidth; + private int windowHeight; + private int windowX; + private int windowY; + private final ConfigLocation configLocation; + + public AppConfig() { + this(new ConfigLocation()); + } + + public AppConfig(ConfigLocation configLocation) { + this.configLocation = configLocation; + windowWidth = DEFAULT_WIDTH; + windowHeight = DEFAULT_HEIGHT; + windowX = DEFAULT_X; + windowY = DEFAULT_Y; + load(); + } + + private void load() { + if (!configLocation.getConfigPropertiesFile().exists()) { + return; + } + + Properties props = new Properties(); + try (FileInputStream fis = new FileInputStream(configLocation.getConfigPropertiesFile())) { + props.load(fis); + + windowWidth = Integer.parseInt(props.getProperty("windowWidth", String.valueOf(DEFAULT_WIDTH))); + windowHeight = Integer.parseInt(props.getProperty("windowHeight", String.valueOf(DEFAULT_HEIGHT))); + windowX = Integer.parseInt(props.getProperty("windowX", String.valueOf(DEFAULT_X))); + windowY = Integer.parseInt(props.getProperty("windowY", String.valueOf(DEFAULT_Y))); + } catch (IOException | NumberFormatException e) { + // Use default values on error + } + } + + public void save() { + configLocation.ensureDirectoryExists(); + + Properties props = new Properties(); + props.setProperty("windowWidth", String.valueOf(windowWidth)); + props.setProperty("windowHeight", String.valueOf(windowHeight)); + props.setProperty("windowX", String.valueOf(windowX)); + props.setProperty("windowY", String.valueOf(windowY)); + + try (FileOutputStream fos = new FileOutputStream(configLocation.getConfigPropertiesFile())) { + props.store(fos, "Llama Runner Configuration"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public int getWindowWidth() { + return windowWidth; + } + + public void setWindowWidth(int windowWidth) { + this.windowWidth = windowWidth; + } + + public int getWindowHeight() { + return windowHeight; + } + + public void setWindowHeight(int windowHeight) { + this.windowHeight = windowHeight; + } + + public int getWindowX() { + return windowX; + } + + public void setWindowX(int windowX) { + this.windowX = windowX; + } + + public int getWindowY() { + return windowY; + } + + public void setWindowY(int windowY) { + this.windowY = windowY; + } +} diff --git a/src/main/java/cz/kamma/llamarunner/CommandBuilder.java b/src/main/java/cz/kamma/llamarunner/CommandBuilder.java new file mode 100644 index 0000000..21b2a2a --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/CommandBuilder.java @@ -0,0 +1,74 @@ +package cz.kamma.llamarunner; + +import java.io.File; + +/** + * Builds llama-server command lines from ModelConfig. + */ +public class CommandBuilder { + + /** + * Builds the command string from a ModelConfig. + * + * @param config the model configuration + * @param modelsDirPath the path to the models directory + * @return the complete command string + */ + public String buildCommand(ModelConfig config, String modelsDirPath) { + StringBuilder cmd = new StringBuilder("llama-server"); + cmd.append(" --host ").append(config.getHost()); + cmd.append(" --port ").append(config.getPort()); + cmd.append(" --parallel ").append(config.getParallel()); + cmd.append(" -t ").append(config.getThreads()); + cmd.append(" -fa ").append(config.isFlashAttention() ? "on" : "off"); + cmd.append(" --temp ").append(config.getTemperature()); + cmd.append(" --top-p ").append(config.getTopP()); + cmd.append(" --top-k ").append(config.getTopK()); + cmd.append(" --min-p ").append(config.getMinP()); + + if (config.isKvUnified()) { + cmd.append(" --kv-unified"); + } + + cmd.append(" --cache-type-k ").append(config.getCacheTypeK()); + cmd.append(" --cache-type-v ").append(config.getCacheTypeV()); + + cmd.append(" --ctx-size ").append(config.getCtxSize()); + + if (!config.isFit()) { + cmd.append(" -ngl ").append(config.getNgl()); + } + + if (config.isFit()) { + cmd.append(" --fit on"); + } + + String modelPath = buildModelPath(config.getModelPath(), modelsDirPath); + cmd.append(" -m ").append(modelPath); + + String kwargsText = config.getChatTemplateKwargs(); + if (kwargsText != null && !kwargsText.trim().isEmpty()) { + cmd.append(" --chat-template-kwargs \""); + cmd.append(kwargsText.replace("\"", "\\\"")); + cmd.append("\""); + } + + return cmd.toString(); + } + + /** + * Builds the full model path. + */ + private String buildModelPath(String modelPath, String modelsDirPath) { + if (modelPath == null || modelPath.trim().isEmpty()) { + return ""; + } + + File modelFile = new File(modelPath); + if (modelFile.isAbsolute()) { + return modelPath; + } + + return new File(modelsDirPath, modelPath).getAbsolutePath(); + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ConfigLocation.java b/src/main/java/cz/kamma/llamarunner/ConfigLocation.java new file mode 100644 index 0000000..f438525 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/ConfigLocation.java @@ -0,0 +1,48 @@ +package cz.kamma.llamarunner; + +import java.io.File; + +/** + * Manages configuration file locations in the user's home directory. + */ +public class ConfigLocation { + + private final File appDir; + private final File configPropertiesFile; + private final File profilesJsonFile; + + public ConfigLocation() { + appDir = new File("."); + configPropertiesFile = new File(appDir, "config.properties"); + profilesJsonFile = new File(appDir, "profiles.json"); + } + + /** + * Ensures the application directory exists. + */ + public void ensureDirectoryExists() { + if (!appDir.exists()) { + appDir.mkdirs(); + } + } + + public File getAppDir() { + return appDir; + } + + public File getConfigPropertiesFile() { + return configPropertiesFile; + } + + public File getProfilesJsonFile() { + return profilesJsonFile; + } + + public String getConfigPropertiesFilePath() { + return configPropertiesFile.getAbsolutePath(); + } + + public String getProfilesJsonFilePath() { + return profilesJsonFile.getAbsolutePath(); + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ConfigValidation.java b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java new file mode 100644 index 0000000..9d29290 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/ConfigValidation.java @@ -0,0 +1,123 @@ +package cz.kamma.llamarunner; + +import java.io.File; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * Validation utilities for configuration inputs. + */ +public class ConfigValidation { + + private static final Gson GSON = new Gson(); + + /** + * Validates numeric fields for valid ranges. + * + * @return a list of error messages (empty 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<>(); + + // Validate port + if (port < 1 || port > 65535) { + errors.add("Port must be between 1 and 65535"); + } + + // Validate parallel + if (parallel < 1) { + errors.add("Parallel must be at least 1"); + } + + // Validate threads + if (threads < 1) { + errors.add("Threads must be at least 1"); + } + + // Validate temperature + if (temperature < 0) { + errors.add("Temperature must be non-negative"); + } + + // Validate topP + if (topP < 0 || topP > 1) { + errors.add("Top P must be between 0 and 1"); + } + + // Validate topK + if (topK < 0) { + errors.add("Top K must be non-negative"); + } + + // Validate minP + if (minP < 0 || minP > 1) { + errors.add("Min P must be between 0 and 1"); + } + + // Validate ctxSize + if (ctxSize < 0) { + errors.add("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()) { + return "Model path cannot be empty"; + } + + File modelFile = new File(modelPath); + if (!modelFile.exists()) { + return "Model file does not exist: " + modelPath; + } + + if (!modelFile.isFile()) { + return "Model path is not a file: " + modelPath; + } + + return null; + } +} diff --git a/src/main/java/cz/kamma/llamarunner/Main.java b/src/main/java/cz/kamma/llamarunner/Main.java new file mode 100644 index 0000000..1eecc13 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/Main.java @@ -0,0 +1,1030 @@ +package cz.kamma.llamarunner; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.Image; +import javax.imageio.ImageIO; +import java.awt.datatransfer.StringSelection; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +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; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +/** + * Main Swing application class for managing llama-server. + */ +public class Main extends JFrame { + + private final AppConfig appConfig; + private final ConfigLocation configLocation; + private final ProfileManager profileManager; + private final CommandBuilder commandBuilder; + private boolean isProfileLoading = false; + private java.awt.event.ActionListener profileChangeListener; + + private JTextField hostField; + private JTextField portField; + private JTextField parallelField; + private JTextField threadsField; + private JCheckBox flashAttnCheckBox; + private JCheckBox kvUnifiedCheckBox; + private JCheckBox fitCheckBox; + private JComboBox cacheTypeKComboBox; + private JComboBox cacheTypeVComboBox; + private JTextField tempField; + private JTextField topPField; + private JTextField topKField; + private JTextField minPField; + private JTextField ctxSizeField; + private JTextField kwargsField; + private JTextField nglField; + private JComboBox modelComboBox; + private JTextArea logArea; + private JTextArea commandPreviewArea; + private JButton browseModelsButton; + private JButton refreshModelsButton; + private JButton saveProfileButton; + private JButton copyCommandButton; + private JButton runCommandButton; + private JButton deleteProfileButton; + private JComboBox profileComboBox; + private String modelsDirPath = System.getProperty("user.home") + "/models"; + + public Main() { + configLocation = new ConfigLocation(); + appConfig = new AppConfig(configLocation); + profileManager = new ProfileManager(configLocation); + commandBuilder = new CommandBuilder(); + + setTitle("Llama Runner"); + setSize(appConfig.getWindowWidth(), appConfig.getWindowHeight()); + + // Set application icon + try { + Image icon = ImageIO.read(getClass().getResource("/llama.png")); + setIconImage(icon); + } catch (Exception e) { + // Icon loading failed, use default + } + + // Set window position if not centered + if (appConfig.getWindowX() >= 0 && appConfig.getWindowY() >= 0) { + setLocation(appConfig.getWindowX(), appConfig.getWindowY()); + } else { + setLocationRelativeTo(null); + } + + JPanel mainPanel = new JPanel(new BorderLayout(8, 8)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + + // Top panel with model and profiles + mainPanel.add(createTopPanel(), BorderLayout.NORTH); + + // Center container - holds config panels and command preview + JPanel centerPanel = new JPanel(new BorderLayout(8, 8)); + + // Configuration panels + centerPanel.add(createConfigPanels(), BorderLayout.CENTER); + + // Command preview panel + centerPanel.add(createCommandPreviewPanel(), BorderLayout.SOUTH); + + // Log panel + mainPanel.add(centerPanel, BorderLayout.CENTER); + + // Log panel (bottom) + mainPanel.add(createLogPanel(), BorderLayout.SOUTH); + + setContentPane(mainPanel); + + // Add window listener to save configuration on close + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + SwingUtilities.invokeLater(() -> { + saveWindowConfig(); + }); + } + }); + + // Load data after GUI is fully initialized + SwingUtilities.invokeLater(() -> { + loadModels(); + loadProfiles(); + updateCommandPreview(); + }); + } + + private JPanel createTopPanel() { + JPanel panel = new JPanel(new BorderLayout(10, 10)); + + // Profile management panel + panel.add(createProfileManagementPanel(), BorderLayout.NORTH); + + // Model selection panel + panel.add(createModelSelectionPanel(), BorderLayout.SOUTH); + + return panel; + } + + private JPanel createModelSelectionPanel() { + JPanel panel = new JPanel(new BorderLayout(5, 5)); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Model")); + + // Model selection row + JPanel modelRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + modelRow.add(new JLabel("Model:")); + modelComboBox = new JComboBox<>(); + modelComboBox.setEditable(true); + modelComboBox.setPreferredSize(new Dimension(300, 25)); + modelComboBox.addActionListener(e -> updateCommandPreview()); + modelRow.add(modelComboBox); + + browseModelsButton = new JButton("Browse"); + browseModelsButton.addActionListener(e -> browseModelsDir()); + modelRow.add(browseModelsButton); + + refreshModelsButton = new JButton("Refresh"); + refreshModelsButton.addActionListener(e -> loadModels()); + modelRow.add(refreshModelsButton); + + JScrollPane scrollPane = new JScrollPane(modelRow); + scrollPane.setBorder(null); + panel.add(scrollPane, BorderLayout.CENTER); + + return panel; + } + + private JPanel createProfileManagementPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Profile")); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(0, 0, 0, 0); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.NONE; + + gbc.gridx = 0; + gbc.gridy = 0; + panel.add(new JLabel("Profile:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + profileComboBox = new JComboBox<>(); + profileComboBox.setEditable(true); + profileComboBox.setMaximumSize(new Dimension(200, 25)); + profileComboBox.setPreferredSize(new Dimension(150, 25)); + profileChangeListener = e -> profileComboBoxChanged(); + profileComboBox.addActionListener(profileChangeListener); + panel.add(profileComboBox, gbc); + + gbc.gridx = 2; + gbc.weightx = 0; + gbc.fill = GridBagConstraints.NONE; + + gbc.gridx = 3; + saveProfileButton = new JButton("Save"); + saveProfileButton.addActionListener(e -> showSaveProfileDialog()); + panel.add(saveProfileButton, gbc); + + gbc.gridx = 4; + JButton saveAsButton = new JButton("Save As..."); + saveAsButton.addActionListener(e -> showSaveAsProfileDialog()); + panel.add(saveAsButton, gbc); + + gbc.gridx = 5; + deleteProfileButton = new JButton("Delete"); + deleteProfileButton.addActionListener(e -> showDeleteProfileDialog()); + panel.add(deleteProfileButton, gbc); + + gbc.gridx = 6; + JButton renameButton = new JButton("Rename"); + renameButton.addActionListener(e -> showRenameProfileDialog()); + panel.add(renameButton, gbc); + + return panel; + } + + private void loadModels() { + String selectedItem = (String) modelComboBox.getSelectedItem(); + + modelComboBox.removeAllItems(); + + File modelsDir = new File(modelsDirPath); + if (modelsDir.exists() && modelsDir.isDirectory()) { + File[] modelFiles = modelsDir.listFiles((dir, name) -> name.endsWith(".gguf")); + if (modelFiles != null) { + List sortedFiles = new ArrayList<>(Arrays.asList(modelFiles)); + sortedFiles.sort((a, b) -> a.getName().compareTo(b.getName())); + for (File file : sortedFiles) { + modelComboBox.addItem(file.getName()); + } + } + } + + if (selectedItem != null && modelComboBox.getItemCount() > 0 + && !modelComboBox.getItemAt(0).equals(selectedItem)) { + modelComboBox.addItem(selectedItem); + } + } + + private void loadProfiles() { + // Remove listener temporarily to prevent triggering during load + profileComboBox.removeActionListener(profileChangeListener); + + try { + List profiles = profileManager.listProfiles(); + profiles.sort(String::compareToIgnoreCase); + for (String name : profiles) { + profileComboBox.addItem(name); + } + + // Re-add listener after loading + profileComboBox.addActionListener(profileChangeListener); + } catch (Exception e) { + e.printStackTrace(); + // Re-add listener even on error + profileComboBox.addActionListener(profileChangeListener); + JOptionPane.showMessageDialog(this, "Error loading profiles: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + } + } + + private void profileComboBoxChanged() { + if (isProfileLoading) { + return; + } + String selectedItem = (String) profileComboBox.getSelectedItem(); + if (selectedItem != null && !selectedItem.isEmpty()) { + showLoadProfileDialog(); + } + } + + private void browseModelsDir() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + fileChooser.setDialogTitle("Select models directory"); + + int result = fileChooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + modelsDirPath = fileChooser.getSelectedFile().getAbsolutePath(); + loadModels(); + } + } + + private void showSaveProfileDialog() { + String currentProfile = (String) profileComboBox.getSelectedItem(); + if (currentProfile == null || currentProfile.isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a profile first!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + try { + saveProfileToName(currentProfile); + JOptionPane.showMessageDialog(this, "Profile saved!"); + + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Error saving: " + e.getMessage() + "\n" + e.toString(), "Error", + JOptionPane.ERROR_MESSAGE); + } + } + + private void saveProfileToName(String name) throws IOException { + ModelConfig config = buildModelConfig(); + profileManager.saveProfile(name, config); + } + + private ModelConfig buildModelConfig() { + ModelConfig config = new ModelConfig(); + config.setHost(hostField.getText()); + config.setPort(Integer.parseInt(portField.getText())); + config.setParallel(Integer.parseInt(parallelField.getText())); + config.setThreads(Integer.parseInt(threadsField.getText())); + config.setFlashAttention(flashAttnCheckBox.isSelected()); + config.setKvUnified(kvUnifiedCheckBox.isSelected()); + config.setFit(fitCheckBox.isSelected()); + config.setCacheTypeK((String) cacheTypeKComboBox.getSelectedItem()); + config.setCacheTypeV((String) cacheTypeVComboBox.getSelectedItem()); + config.setTemperature(Double.parseDouble(tempField.getText())); + config.setTopP(Double.parseDouble(topPField.getText())); + config.setTopK(Integer.parseInt(topKField.getText())); + config.setMinP(Double.parseDouble(minPField.getText())); + config.setCtxSize(Integer.parseInt(ctxSizeField.getText())); + String modelName = (String) modelComboBox.getSelectedItem(); + config.setModelPath(modelName != null ? new File(modelsDirPath, modelName).getAbsolutePath() : ""); + config.setChatTemplateKwargs(kwargsField.getText()); + config.setNgl(Integer.parseInt(nglField.getText())); + return config; + } + + private void showLoadProfileDialog() { + String name = (String) profileComboBox.getSelectedItem(); + if (name == null || name.isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a profile!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + try { + isProfileLoading = true; + ModelConfig config = profileManager.loadProfile(name); + + hostField.setText(config.getHost()); + portField.setText(String.valueOf(config.getPort())); + parallelField.setText(String.valueOf(config.getParallel())); + threadsField.setText(String.valueOf(config.getThreads())); + flashAttnCheckBox.setSelected(config.isFlashAttention()); + kvUnifiedCheckBox.setSelected(config.isKvUnified()); + fitCheckBox.setSelected(config.isFit()); + toggleFitMode(); + cacheTypeKComboBox.setSelectedItem(config.getCacheTypeK()); + cacheTypeVComboBox.setSelectedItem(config.getCacheTypeV()); + tempField.setText(String.valueOf(config.getTemperature())); + topPField.setText(String.valueOf(config.getTopP())); + topKField.setText(String.valueOf(config.getTopK())); + minPField.setText(String.valueOf(config.getMinP())); + ctxSizeField.setText(String.valueOf(config.getCtxSize())); + String modelName = config.getModelPath() != null + ? new File(config.getModelPath()).getName() + : ""; + modelComboBox.setSelectedItem(modelName); + + kwargsField.setText(config.getChatTemplateKwargs()); + nglField.setText(String.valueOf(config.getNgl())); + + updateCommandPreview(); + + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Error loading: " + e.getMessage() + "\n" + e.toString(), "Error", + JOptionPane.ERROR_MESSAGE); + } finally { + isProfileLoading = false; + } + } + + private void showSaveAsProfileDialog() { + String modelName = (String) modelComboBox.getSelectedItem(); + String defaultName = modelName != null ? modelName : ""; + + String[] result = new String[1]; + + 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()) { + return; // Cancelled by user + } + + try { + // Validate profile name + String validationError = ProfileValidator.validateProfileName(newName); + if (validationError != null) { + JOptionPane.showMessageDialog(this, validationError, "Invalid profile name", + JOptionPane.ERROR_MESSAGE); + return; + } + + // Check if profile already exists + if (profileManager.profileExists(newName)) { + int confirmResult = JOptionPane.showConfirmDialog(this, + "Profile \"" + newName + "\" already exists. Overwrite it?", + "Save as...", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + + if (confirmResult != JOptionPane.YES_OPTION) { + return; + } + } + + saveProfileToName(newName); + loadProfiles(); + profileComboBox.setSelectedItem(newName); + JOptionPane.showMessageDialog(this, "Profile saved!"); + + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Error saving: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + } + } + + private void showDeleteProfileDialog() { + String name = (String) profileComboBox.getSelectedItem(); + if (name == null || name.isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a profile!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + int result = JOptionPane.showConfirmDialog(this, + "Are you sure you want to delete profile \"" + name + "\"?", + "Delete profile", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + + if (result != JOptionPane.YES_OPTION) { + return; + } + + try { + if (!profileManager.profileExists(name)) { + JOptionPane.showMessageDialog(this, "Profile does not exist!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + profileManager.deleteProfile(name); + loadProfiles(); + JOptionPane.showMessageDialog(this, "Profile was deleted."); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Error deleting: " + e.getMessage() + "\n" + e.toString(), + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + private void showRenameProfileDialog() { + String oldName = (String) profileComboBox.getSelectedItem(); + if (oldName == null || oldName.isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a profile!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + String[] result = new String[1]; + + 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()) { + return; // Cancelled by user + } + + try { + // Validate new profile name + String validationError = ProfileValidator.validateProfileName(newName); + if (validationError != null) { + JOptionPane.showMessageDialog(this, validationError, "Invalid profile name", + JOptionPane.ERROR_MESSAGE); + return; + } + + // Check if new name already exists + if (profileManager.profileExists(newName)) { + JOptionPane.showMessageDialog(this, "Profile already exists: " + newName, "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + profileManager.renameProfile(oldName, newName); + loadProfiles(); + profileComboBox.setSelectedItem(newName); + JOptionPane.showMessageDialog(this, "Profile renamed to \"" + newName + "\"."); + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Error renaming: " + e.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + } + } + + private JPanel createConfigPanels() { + JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // Create a scrollable panel for the configuration panels + JPanel configPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(4, 4, 4, 4); + gbc.fill = GridBagConstraints.HORIZONTAL; + + // Performance panel + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + gbc.weighty = 1.0; + configPanel.add(createPerformancePanel(), gbc); + + // Sampling panel + gbc.gridx = 0; + gbc.gridy = 1; + gbc.gridwidth = 2; + configPanel.add(createSamplingPanel(), gbc); + + // Connection panel + gbc.gridx = 0; + gbc.gridy = 2; + gbc.gridwidth = 2; + gbc.weightx = 1.0; + configPanel.add(createConnectionPanel(), gbc); + + JScrollPane scrollPane = new JScrollPane(configPanel); + scrollPane.setBorder(null); + scrollPane.setViewportView(configPanel); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + + mainPanel.add(scrollPane, BorderLayout.CENTER); + + return mainPanel; + } + + private JPanel createConnectionPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Connection")); + + 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; + panel.add(new JLabel("Host:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + hostField = new JTextField("0.0.0.0", 15); + hostField.setCaretColor(Color.WHITE); + hostField.addActionListener(e -> updateCommandPreview()); + panel.add(hostField, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + panel.add(new JLabel("Port:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + portField = new JTextField("3080", 10); + portField.setCaretColor(Color.WHITE); + portField.addActionListener(e -> updateCommandPreview()); + panel.add(portField, gbc); + + return panel; + } + + private JPanel createPerformancePanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Performance")); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(3, 4, 3, 4); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + + gbc.gridx = 0; + gbc.gridy = 0; + panel.add(new JLabel("Parallel:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + parallelField = new JTextField("1", 10); + parallelField.setCaretColor(Color.WHITE); + parallelField.addActionListener(e -> updateCommandPreview()); + panel.add(parallelField, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + panel.add(new JLabel("Threads:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + threadsField = new JTextField("99", 10); + threadsField.setCaretColor(Color.WHITE); + threadsField.addActionListener(e -> updateCommandPreview()); + panel.add(threadsField, gbc); + + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weightx = 0; + panel.add(new JLabel(), gbc); + + gbc.gridx = 1; + gbc.gridy = 2; + gbc.weightx = 1.0; + panel.add(new JLabel(), gbc); + + gbc.gridx = 0; + gbc.gridy = 3; + gbc.weightx = 0; + flashAttnCheckBox = new JCheckBox("Flash Attention", true); + flashAttnCheckBox.addActionListener(e -> updateCommandPreview()); + panel.add(flashAttnCheckBox, gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + panel.add(new JLabel(), gbc); + + gbc.gridx = 0; + gbc.gridy = 4; + gbc.weightx = 0; + kvUnifiedCheckBox = new JCheckBox("KV Unified", true); + kvUnifiedCheckBox.addActionListener(e -> updateCommandPreview()); + panel.add(kvUnifiedCheckBox, gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + panel.add(new JLabel(), gbc); + + gbc.gridx = 0; + gbc.gridy = 5; + gbc.weightx = 0; + panel.add(new JLabel("Cache K:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + cacheTypeKComboBox = new JComboBox<>(new String[] { "bf16", "f16", "f32", "f8", "q8_0" }); + cacheTypeKComboBox.setSelectedIndex(0); + cacheTypeKComboBox.addActionListener(e -> updateCommandPreview()); + panel.add(cacheTypeKComboBox, gbc); + + gbc.gridx = 0; + gbc.gridy = 6; + gbc.weightx = 0; + panel.add(new JLabel("Cache V:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + cacheTypeVComboBox = new JComboBox<>(new String[] { "bf16", "f16", "f32", "f8", "q8_0" }); + cacheTypeVComboBox.setSelectedIndex(0); + cacheTypeVComboBox.addActionListener(e -> updateCommandPreview()); + panel.add(cacheTypeVComboBox, gbc); + + gbc.gridx = 0; + gbc.gridy = 7; + gbc.weightx = 0; + panel.add(new JLabel("GPU Layers:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + nglField = new JTextField("999", 10); + nglField.setCaretColor(Color.WHITE); + nglField.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(); + } + }); + panel.add(nglField, gbc); + + // Fit parameter + gbc.gridx = 0; + gbc.gridy = 8; + gbc.weightx = 0; + panel.add(new JLabel("Fit:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + fitCheckBox = new JCheckBox("Fit", false); + fitCheckBox.addActionListener(e -> toggleFitMode()); + panel.add(fitCheckBox, gbc); + + return panel; + } + + private JPanel createSamplingPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Sampling")); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(3, 4, 3, 4); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + + gbc.gridx = 0; + gbc.gridy = 0; + panel.add(new JLabel("Temperature:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + tempField = new JTextField("0.6", 10); + tempField.setCaretColor(Color.WHITE); + tempField.addActionListener(e -> updateCommandPreview()); + panel.add(tempField, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + panel.add(new JLabel("Top P:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + topPField = new JTextField("0.95", 10); + topPField.setCaretColor(Color.WHITE); + topPField.addActionListener(e -> updateCommandPreview()); + panel.add(topPField, gbc); + + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weightx = 0; + panel.add(new JLabel("Top K:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + topKField = new JTextField("20", 10); + topKField.setCaretColor(Color.WHITE); + topKField.addActionListener(e -> updateCommandPreview()); + panel.add(topKField, gbc); + + gbc.gridx = 0; + gbc.gridy = 3; + gbc.weightx = 0; + panel.add(new JLabel("Min P:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + minPField = new JTextField("0.00", 10); + minPField.setCaretColor(Color.WHITE); + minPField.addActionListener(e -> updateCommandPreview()); + panel.add(minPField, gbc); + + gbc.gridx = 0; + gbc.gridy = 4; + gbc.weightx = 0; + panel.add(new JLabel("Context Size:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + ctxSizeField = new JTextField("180000", 10); + ctxSizeField.setCaretColor(Color.WHITE); + ctxSizeField.addActionListener(e -> updateCommandPreview()); + panel.add(ctxSizeField, gbc); + + gbc.gridx = 0; + gbc.gridy = 6; + gbc.weightx = 0; + panel.add(new JLabel("Kwargs:"), gbc); + + gbc.gridx = 1; + gbc.gridy = 6; + 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(); + } + }); + panel.add(kwargsField, gbc); + + return panel; + } + + private JPanel createCommandPreviewPanel() { + JPanel panel = new JPanel(new BorderLayout(4, 4)); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Resulting command")); + + commandPreviewArea = new JTextArea(); + commandPreviewArea.setEditable(false); + commandPreviewArea.setFont(new Font("Monospaced", Font.PLAIN, 11)); + commandPreviewArea.setLineWrap(true); + commandPreviewArea.setWrapStyleWord(true); + commandPreviewArea.setBackground(new Color(240, 240, 240)); + commandPreviewArea.setForeground(Color.BLACK); + + JScrollPane scrollPane = new JScrollPane(commandPreviewArea); + scrollPane.setPreferredSize(new Dimension(0, 60)); + panel.add(scrollPane, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0)); + copyCommandButton = new JButton("Copy"); + copyCommandButton.addActionListener(e -> copyCommandToClipboard()); + buttonPanel.add(copyCommandButton); + + runCommandButton = new JButton("Run"); + runCommandButton.addActionListener(e -> runCommand()); + buttonPanel.add(runCommandButton); + + panel.add(buttonPanel, BorderLayout.EAST); + + return panel; + } + + private void updateCommandPreview() { + String command = buildCommand(); + commandPreviewArea.setText(command); + } + + private void toggleFitMode() { + nglField.setEnabled(!fitCheckBox.isSelected()); + updateCommandPreview(); + } + + private void copyCommandToClipboard() { + String command = buildCommand(); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(command), null); + } + + private void runCommand() { + String command = buildCommand(); + logArea.setText("========================================\n"); + logArea.append("Command executed:\n"); + logArea.append(command + "\n"); + logArea.append("========================================\n\n"); + + // Use x-terminal-emulator for better compatibility + String terminalCommand = "gnome-terminal -- bash -ic '" + command + "'"; + + logArea.append("Terminal command: " + terminalCommand + "\n"); + + try { + Runtime.getRuntime().exec(new String[] { "bash", "-c", terminalCommand }); + + logArea.append("Command executed in terminal.\n"); + + } catch (IOException e) { + e.printStackTrace(); + logArea.append("Error executing command: " + e.getMessage() + "\n"); + } + } + + private JPanel createLogPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.WHITE), "Log")); + + logArea = new JTextArea(); + logArea.setEditable(false); + logArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + logArea.setRows(10); + + JScrollPane scrollPane = new JScrollPane(logArea); + scrollPane.setPreferredSize(new Dimension(0, 150)); + panel.add(scrollPane, BorderLayout.CENTER); + + return panel; + } + + private String buildCommand() { + return commandBuilder.buildCommand(buildModelConfig(), modelsDirPath); + } + + private void saveWindowConfig() { + appConfig.setWindowWidth(getWidth()); + appConfig.setWindowHeight(getHeight()); + + // Only save position if window is not maximized + if (getExtendedState() == 0) { + Point location = getLocation(); + appConfig.setWindowX((int) location.getX()); + appConfig.setWindowY((int) location.getY()); + } + + appConfig.save(); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + new Main().setVisible(true); + }); + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ModelConfig.java b/src/main/java/cz/kamma/llamarunner/ModelConfig.java new file mode 100644 index 0000000..3d02548 --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/ModelConfig.java @@ -0,0 +1,129 @@ +package cz.kamma.llamarunner; + +import java.io.Serializable; + +/** + * Class for storing llama-server configuration. + */ +public class ModelConfig implements Serializable { + private static final long serialVersionUID = 1L; + + private String host; + private int port; + private int parallel; + private int threads; + private boolean flashAttention; + private boolean kvUnified; + private String cacheTypeK; + private String cacheTypeV; + private double temperature; + private double topP; + private int topK; + private double minP; + private int ctxSize; + private boolean enableThinking; + private String modelPath; + private String chatTemplateKwargs; + private int ngl; + private boolean fit; + + public ModelConfig() { + this.host = "0.0.0.0"; + this.port = 3080; + this.parallel = 1; + this.threads = 99; + this.flashAttention = true; + this.kvUnified = true; + this.cacheTypeK = "bf16"; + this.cacheTypeV = "bf16"; + this.temperature = 0.6; + this.topP = 0.95; + this.topK = 20; + this.minP = 0.00; + this.ctxSize = 180000; + this.enableThinking = false; + this.modelPath = ""; + this.chatTemplateKwargs = ""; + this.ngl = 999; + this.fit = false; + } + + // Getters and setters + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public int getPort() { return port; } + public void setPort(int port) { this.port = port; } + + public int getParallel() { return parallel; } + public void setParallel(int parallel) { this.parallel = parallel; } + + public int getThreads() { return threads; } + public void setThreads(int threads) { this.threads = threads; } + + public boolean isFlashAttention() { return flashAttention; } + public void setFlashAttention(boolean flashAttention) { this.flashAttention = flashAttention; } + + public boolean isKvUnified() { return kvUnified; } + public void setKvUnified(boolean kvUnified) { this.kvUnified = kvUnified; } + + public String getCacheTypeK() { return cacheTypeK; } + public void setCacheTypeK(String cacheTypeK) { this.cacheTypeK = cacheTypeK; } + + public String getCacheTypeV() { return cacheTypeV; } + public void setCacheTypeV(String cacheTypeV) { this.cacheTypeV = cacheTypeV; } + + public double getTemperature() { return temperature; } + public void setTemperature(double temperature) { this.temperature = temperature; } + + public double getTopP() { return topP; } + public void setTopP(double topP) { this.topP = topP; } + + public int getTopK() { return topK; } + public void setTopK(int topK) { this.topK = topK; } + + public double getMinP() { return minP; } + public void setMinP(double minP) { this.minP = minP; } + + public int getCtxSize() { return ctxSize; } + public void setCtxSize(int ctxSize) { this.ctxSize = ctxSize; } + + public boolean isEnableThinking() { return enableThinking; } + public void setEnableThinking(boolean enableThinking) { this.enableThinking = enableThinking; } + + public String getModelPath() { return modelPath; } + public void setModelPath(String modelPath) { this.modelPath = modelPath; } + + public String getChatTemplateKwargs() { return chatTemplateKwargs; } + public void setChatTemplateKwargs(String chatTemplateKwargs) { this.chatTemplateKwargs = chatTemplateKwargs; } + + public int getNgl() { return ngl; } + public void setNgl(int ngl) { this.ngl = ngl; } + + public boolean isFit() { return fit; } + public void setFit(boolean fit) { this.fit = fit; } + + @Override + public String toString() { + return "ModelConfig{" + + "host='" + host + '\'' + + ", port=" + port + + ", parallel=" + parallel + + ", threads=" + threads + + ", flashAttention=" + flashAttention + + ", kvUnified=" + kvUnified + + ", cacheTypeK='" + cacheTypeK + '\'' + + ", cacheTypeV='" + cacheTypeV + '\'' + + ", temperature=" + temperature + + ", topP=" + topP + + ", topK=" + topK + + ", minP=" + minP + + ", ctxSize=" + ctxSize + + ", enableThinking=" + enableThinking + + ", modelPath='" + modelPath + '\'' + + ", chatTemplateKwargs='" + chatTemplateKwargs + '\'' + + ", ngl=" + ngl + + ", fit=" + fit + + '}'; + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ProfileManager.java b/src/main/java/cz/kamma/llamarunner/ProfileManager.java new file mode 100644 index 0000000..274f99b --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/ProfileManager.java @@ -0,0 +1,137 @@ +package cz.kamma.llamarunner; + +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +/** + * Profile management using JSON file in user's home directory. + */ +public class ProfileManager { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private final ConfigLocation configLocation; + + public ProfileManager() { + this(new ConfigLocation()); + } + + public ProfileManager(ConfigLocation configLocation) { + this.configLocation = configLocation; + } + + /** + * Saves profile to JSON file. + */ + public void saveProfile(String name, ModelConfig config) throws IOException { + configLocation.ensureDirectoryExists(); + Map profiles = loadAllProfiles(); + profiles.put(name, config); + + try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) { + GSON.toJson(profiles, writer); + } + } + + /** + * Loads profile from JSON file. + */ + public ModelConfig loadProfile(String name) throws IOException { + Map profiles = loadAllProfiles(); + ModelConfig config = profiles.get(name); + if (config == null) { + throw new IOException("Profile does not exist: " + name); + } + return config; + } + + /** + * Deletes profile from JSON file. + */ + public void deleteProfile(String name) throws IOException { + Map profiles = loadAllProfiles(); + if (!profiles.containsKey(name)) { + throw new IOException("Profile does not exist: " + name); + } + profiles.remove(name); + + try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) { + GSON.toJson(profiles, writer); + } + } + + /** + * Returns the path to the profiles file. + */ + public String getProfilesFilePath() { + return configLocation.getProfilesJsonFilePath(); + } + + /** + * Returns the list of profile names. + */ + public List listProfiles() throws IOException { + Map profiles = loadAllProfiles(); + return profiles != null ? new ArrayList<>(profiles.keySet()) : new ArrayList<>(); + } + + /** + * Checks if profile exists. + */ + public boolean profileExists(String name) { + try { + Map profiles = loadAllProfiles(); + return profiles.containsKey(name); + } catch (IOException e) { + return false; + } + } + + /** + * Renames a profile from oldName to newName. + */ + public void renameProfile(String oldName, String newName) throws IOException { + Map profiles = loadAllProfiles(); + if (!profiles.containsKey(oldName)) { + throw new IOException("Profile does not exist: " + oldName); + } + if (oldName.equals(newName)) { + throw new IOException("Old and new names must be different."); + } + if (profiles.containsKey(newName)) { + throw new IOException("Profile already exists: " + newName); + } + + ModelConfig config = profiles.remove(oldName); + profiles.put(newName, config); + + try (FileWriter writer = new FileWriter(configLocation.getProfilesJsonFile())) { + GSON.toJson(profiles, writer); + } + } + + /** + * Loads all profiles from JSON file. + */ + private Map loadAllProfiles() throws IOException { + if (!configLocation.getProfilesJsonFile().exists()) { + return new HashMap<>(); + } + + try (FileReader reader = new FileReader(configLocation.getProfilesJsonFile())) { + java.lang.reflect.Type mapType = new TypeToken>() {}.getType(); + Map profiles = GSON.fromJson(reader, mapType); + return profiles != null ? profiles : new HashMap<>(); + } catch (Exception e) { + return new HashMap<>(); + } + } +} diff --git a/src/main/java/cz/kamma/llamarunner/ProfileValidator.java b/src/main/java/cz/kamma/llamarunner/ProfileValidator.java new file mode 100644 index 0000000..62014cf --- /dev/null +++ b/src/main/java/cz/kamma/llamarunner/ProfileValidator.java @@ -0,0 +1,51 @@ +package cz.kamma.llamarunner; + +import java.util.regex.Pattern; + +/** + * Validation utilities for profile names. + */ +public class ProfileValidator { + + private static final int MAX_NAME_LENGTH = 100; + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$"); + + /** + * Validates a profile name. + * + * @param name the profile name to validate + * @return error message if invalid, null if valid + */ + public static String validateProfileName(String name) { + if (name == null || name.trim().isEmpty()) { + return "Profile name cannot be empty"; + } + + String trimmedName = name.trim(); + + if (trimmedName.length() > MAX_NAME_LENGTH) { + return "Profile name must be at most " + MAX_NAME_LENGTH + " characters"; + } + + if (!VALID_NAME_PATTERN.matcher(trimmedName).matches()) { + return "Profile name can only contain letters, numbers, underscores, and hyphens"; + } + + 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)); + } +} diff --git a/src/main/resources/llama.png b/src/main/resources/llama.png new file mode 100644 index 0000000..1b9d293 Binary files /dev/null and b/src/main/resources/llama.png differ