diff --git a/lm-launcher.jar b/lm-launcher.jar index 8ffc1c1..b729e03 100644 Binary files a/lm-launcher.jar and b/lm-launcher.jar differ diff --git a/src/de/extio/lm_launcher/Data.java b/src/de/extio/lm_launcher/Data.java index aca833e..bd6e35a 100644 --- a/src/de/extio/lm_launcher/Data.java +++ b/src/de/extio/lm_launcher/Data.java @@ -5,6 +5,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -45,7 +47,7 @@ public class Data { for (int i = 0; i < modelData.models().size(); i++) { final Model model = modelData.models.get(i); if (model.maxContextSize() < model.contextSize() || model.promptTemplate() == null) { - modelData.models.set(i, new Model(model.path(), model.contextSize(), model.contextSize(), model.gpuLayers(), model.threads(), Objects.requireNonNullElse(model.promptTemplate(), ""))); + modelData.models.set(i, new Model(model.path(), model.contextSize(), model.contextSize(), model.gpuLayers(), model.threads(), Objects.requireNonNullElse(model.promptTemplate(), ""), model.ctime())); } } sortModels(); @@ -83,8 +85,9 @@ public class Data { static void sortModels() { modelData.models().sort(Comparator - .comparing(Data::modelName, String.CASE_INSENSITIVE_ORDER) - .thenComparing(Model::path)); + .comparing(m -> Path.of(m.path()).getParent().toString()) + .thenComparing(Model::ctime).reversed() + .thenComparing(Data::modelName, String.CASE_INSENSITIVE_ORDER)); } static void saveApps() { @@ -103,24 +106,57 @@ public class Data { } static List scanModels() { - final List result = new ArrayList<>(); + return scanModelPaths().stream().map(p -> p.toString()).toList(); + } + + static List scanModelPaths() { + final List result = new ArrayList<>(); try { try (var paths = Files.walk(Path.of(props.getProperty("models")))) { paths .filter(Files::isRegularFile) .filter(p -> p.getFileName().toString().endsWith(".gguf")) .map(Path::toAbsolutePath) - .map(Path::toString) - .sorted() + .map(Path::normalize) .forEach(result::add); } } catch (final IOException e) { e.printStackTrace(); } + // Sort by parent path (alphabetical), then by file ctime within the same directory + result.sort(Comparator + .comparing(p -> p.getParent().toString()) + .thenComparing(p -> { + try { + return Files.readAttributes(p, BasicFileAttributes.class).creationTime(); + } + catch (final IOException e) { + return FileTime.fromMillis(0); + } + }).reversed() + .thenComparing(Path::toString)); return result; } + static Model findModelByPath(final String path) { + return modelData.models().stream() + .filter(m -> m.path().equals(path)) + .findFirst() + .orElse(null); + } + + static Model defaultModel(final String path) { + long ctime = 0; + try { + ctime = Files.readAttributes(Path.of(path), BasicFileAttributes.class).creationTime().toMillis(); + } + catch (final IOException e) { + // Use 0 as fallback + } + return new Model(path, 16000, 128000, 99, 6, "", ctime); + } + static record ModelData(List models) { } diff --git a/src/de/extio/lm_launcher/LaunchFrame.java b/src/de/extio/lm_launcher/LaunchFrame.java index cb4e532..e228661 100644 --- a/src/de/extio/lm_launcher/LaunchFrame.java +++ b/src/de/extio/lm_launcher/LaunchFrame.java @@ -1,6 +1,7 @@ package de.extio.lm_launcher; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridLayout; @@ -9,7 +10,9 @@ import java.awt.event.ActionListener; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.swing.AbstractCellEditor; import javax.swing.Box; @@ -27,6 +30,7 @@ import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JSplitPane; import javax.swing.JTable; +import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.WindowConstants; @@ -34,6 +38,11 @@ import javax.swing.border.EmptyBorder; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; public class LaunchFrame extends JFrame { @@ -57,7 +66,7 @@ public class LaunchFrame extends JFrame { JList list; - JList list_1; + JTree modelTree; JButton btnDelete; @@ -77,8 +86,6 @@ public class LaunchFrame extends JFrame { DefaultListModel listModel; - DefaultListModel list1Model; - Component horizontalStrut; JSplitPane splitPane_1; @@ -133,50 +140,33 @@ public class LaunchFrame extends JFrame { { this.panel = new JPanel(); this.contentPane.add(this.panel, BorderLayout.SOUTH); - { - this.btnAdd = new JButton("Add"); - this.btnAdd.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent e) { - final SelectModelDialog smDialog = new SelectModelDialog(LaunchFrame.this, path -> { - final ModelPropertiesDialog mpDialog = new ModelPropertiesDialog(LaunchFrame.this, path, null, model -> { - Data.modelData.models().add(model); - try { - Data.saveModels(); - } - finally { - LaunchFrame.this.refresh(); - } - }); - mpDialog.setLocationRelativeTo(LaunchFrame.this); - mpDialog.setVisible(true); - }); - smDialog.setLocationRelativeTo(LaunchFrame.this); - smDialog.setVisible(true); - } - }); - this.panel.add(this.btnAdd); - } { this.btnEdit = new JButton("Edit"); this.btnEdit.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { - final int index = LaunchFrame.this.list_1.getSelectedIndex(); - if (index == -1) { + final String selectedPath = LaunchFrame.this.getSelectedModelPath(); + if (selectedPath == null) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } - final Model editModel = Data.modelData.models().get(index); - final ModelPropertiesDialog mpDialog = new ModelPropertiesDialog(LaunchFrame.this, editModel.path(), editModel, model -> { - Data.modelData.models().set(index, model); + final Model existingModel = Data.findModelByPath(selectedPath); + final Model editModel = existingModel != null ? existingModel : Data.defaultModel(selectedPath); + final ModelPropertiesDialog mpDialog = new ModelPropertiesDialog(LaunchFrame.this, selectedPath, editModel, model -> { + // Replace or add model params + final int idx = Data.modelData.models().indexOf(existingModel); + if (idx >= 0) { + Data.modelData.models().set(idx, model); + } + else { + Data.modelData.models().add(model); + } try { Data.saveModels(); } finally { - LaunchFrame.this.refresh(); + LaunchFrame.this.refreshModelSliders(model); } }); mpDialog.setLocationRelativeTo(LaunchFrame.this); @@ -185,51 +175,6 @@ public class LaunchFrame extends JFrame { }); this.panel.add(this.btnEdit); } - { - this.btnDelete = new JButton("Delete"); - this.btnDelete.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent e) { - final int index = LaunchFrame.this.list_1.getSelectedIndex(); - if (index == -1) { - JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE); - return; - } - Data.modelData.models().remove(index); - try { - Data.saveModels(); - } - finally { - LaunchFrame.this.refresh(); - } - } - }); - this.panel.add(this.btnDelete); - } - { - this.btnClone = new JButton("Clone"); - this.btnClone.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent e) { - final int index = LaunchFrame.this.list_1.getSelectedIndex(); - if (index == -1) { - JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE); - return; - } - final Model model = Data.modelData.models().get(index); - Data.modelData.models().add(index, new Model(model.path(), model.contextSize(), model.maxContextSize(), model.gpuLayers(), model.threads(), model.promptTemplate())); - try { - Data.saveModels(); - } - finally { - LaunchFrame.this.refresh(); - } - } - }); - this.panel.add(this.btnClone); - } } { this.panel_1 = new JPanel(); @@ -289,32 +234,26 @@ public class LaunchFrame extends JFrame { this.scrollPane_1 = new JScrollPane(); this.splitPane_2.setLeftComponent(this.scrollPane_1); - this.list1Model = new DefaultListModel<>(); - this.list_1 = new JList(this.list1Model); - this.list_1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - this.list_1.getSelectionModel().addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - final DefaultListSelectionModel selectionModel = (DefaultListSelectionModel) e.getSource(); - if (selectionModel.getMinSelectionIndex() > -1) { - final Model model = Data.modelData.models().get(selectionModel.getMinSelectionIndex()); - - this.slider_context.setMaximum(model.maxContextSize()); - this.slider_context.setMajorTickSpacing(model.maxContextSize() / 4); - this.slider_context.setMinorTickSpacing(model.maxContextSize() / 16); - this.slider_context.setValue(model.contextSize()); - this.slider_context.setLabelTable(this.slider_context.createStandardLabels(model.maxContextSize() / 4)); - this.slider_context.setSnapToTicks(true); - this.slider_context.repaint(); - - this.slider_threads.setValue(model.threads()); - - this.slider_gpu.setValue(model.gpuLayers()); - - this.lblPromptTemplate_1.setText(model.promptTemplate()); - } + + // Tree view for models + this.modelTree = new JTree(new DefaultMutableTreeNode("Models")); + this.modelTree.setRootVisible(false); + this.modelTree.setShowsRootHandles(true); + this.modelTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + this.modelTree.setCellRenderer(new ModelTreeCellRenderer()); + // Set smaller font for tree to account for UI scaling + this.modelTree.setFont(this.modelTree.getFont().deriveFont(10f)); + this.modelTree.addTreeSelectionListener(e -> { + final DefaultMutableTreeNode node = (DefaultMutableTreeNode) this.modelTree.getLastSelectedPathComponent(); + if (node != null && node.isLeaf() && node.getUserObject() instanceof Path) { + final Path p = (Path) node.getUserObject(); + final Model saved = Data.findModelByPath(p.toString()); + final Model model = saved != null ? saved : Data.defaultModel(p.toString()); + this.refreshModelSliders(model); } }); - this.scrollPane_1.setViewportView(this.list_1); + this.scrollPane_1.setViewportView(this.modelTree); + { this.panel_3 = new JPanel(); this.splitPane_2.setRightComponent(this.panel_3); @@ -498,16 +437,18 @@ public class LaunchFrame extends JFrame { final App app = Data.appData.apps().get(AppIndex); Model model = null; - final int modelIndex = LaunchFrame.this.list_1.getSelectedIndex(); - if (modelIndex > -1) { - final Model template = Data.modelData.models().get(modelIndex); + final String selectedPath = LaunchFrame.this.getSelectedModelPath(); + if (selectedPath != null) { + final Model saved = Data.findModelByPath(selectedPath); + final Model template = saved != null ? saved : Data.defaultModel(selectedPath); model = new Model( template.path(), LaunchFrame.this.slider_context.getValue(), template.maxContextSize(), LaunchFrame.this.slider_gpu.getValue(), LaunchFrame.this.slider_threads.getValue(), - template.promptTemplate()); + template.promptTemplate(), + template.ctime()); } final List argumentOverrides = new ArrayList<>(); @@ -550,28 +491,97 @@ public class LaunchFrame extends JFrame { this.refresh(); } + /** Returns the absolute path string of the currently selected model leaf node, or null. */ + private String getSelectedModelPath() { + final DefaultMutableTreeNode node = (DefaultMutableTreeNode) this.modelTree.getLastSelectedPathComponent(); + if (node != null && node.isLeaf() && node.getUserObject() instanceof Path) { + return ((Path) node.getUserObject()).toString(); + } + return null; + } + + private void refreshModelSliders(final Model model) { + this.slider_context.setMaximum(model.maxContextSize()); + this.slider_context.setMajorTickSpacing(Math.max(1, model.maxContextSize() / 4)); + this.slider_context.setMinorTickSpacing(Math.max(1, model.maxContextSize() / 16)); + this.slider_context.setValue(model.contextSize()); + this.slider_context.setLabelTable(this.slider_context.createStandardLabels(Math.max(1, model.maxContextSize() / 4))); + this.slider_context.setSnapToTicks(true); + this.slider_context.repaint(); + + this.slider_threads.setValue(model.threads()); + this.slider_gpu.setValue(model.gpuLayers()); + this.lblPromptTemplate_1.setText(model.promptTemplate() != null && !model.promptTemplate().isEmpty() ? model.promptTemplate() : "-"); + } + private void refresh() { - Data.sortModels(); + // Rebuild apps list this.listModel.clear(); Data.appData.apps() .stream() .map(app -> app.path().getFileName() + " " + app.path().getParent() + " " + app.interpreter()) .forEach(this.listModel::addElement); - this.list1Model.clear(); - Data.modelData.models() - .stream() - .map(model -> Path.of(model.path()).getFileName() + "; ctx " + model.contextSize() + "/" + model.maxContextSize()) - .forEach(this.list1Model::addElement); - if (this.table.getCellEditor() != null) { this.table.getCellEditor().stopCellEditing(); } this.appArgumentsTableModel.setRowCount(0); + // Rebuild model tree from filesystem + final String baseStr = Data.props.getProperty("models"); + if (baseStr != null) { + final Path base = Path.of(baseStr).toAbsolutePath().normalize(); + final DefaultMutableTreeNode root = new DefaultMutableTreeNode(base); + final Map nodeMap = new HashMap<>(); + nodeMap.put(base, root); + + for (final Path modelPath : Data.scanModelPaths()) { + final Path rel = base.relativize(modelPath); + DefaultMutableTreeNode current = root; + Path currentAbs = base; + for (int i = 0; i < rel.getNameCount(); i++) { + currentAbs = currentAbs.resolve(rel.getName(i)); + if (!nodeMap.containsKey(currentAbs)) { + final DefaultMutableTreeNode node = new DefaultMutableTreeNode(currentAbs); + current.add(node); + nodeMap.put(currentAbs, node); + } + current = nodeMap.get(currentAbs); + } + } + + this.modelTree.setModel(new DefaultTreeModel(root)); + } + this.repaint(); } + /** Tree cell renderer: shows only the last path component (file/folder name). */ + static class ModelTreeCellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, + final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + if (value instanceof DefaultMutableTreeNode) { + final Object userObj = ((DefaultMutableTreeNode) value).getUserObject(); + if (userObj instanceof Path) { + final Path p = (Path) userObj; + final Path filename = p.getFileName(); + setText(filename != null ? filename.toString() : p.toString()); + // Highlight models with saved params + if (leaf) { + final Model saved = Data.findModelByPath(p.toString()); + if (saved != null) { + setForeground(sel ? Color.WHITE : new Color(0, 100, 0)); + } + } + } + } + return this; + } + } + static class AppArgumentsJCheckBoxRenderer extends JCheckBox implements TableCellRenderer { private final DefaultTableModel model; @@ -617,4 +627,4 @@ public class LaunchFrame extends JFrame { } } -} \ No newline at end of file +} diff --git a/src/de/extio/lm_launcher/Model.java b/src/de/extio/lm_launcher/Model.java index a9b4229..089b959 100644 --- a/src/de/extio/lm_launcher/Model.java +++ b/src/de/extio/lm_launcher/Model.java @@ -1,4 +1,4 @@ package de.extio.lm_launcher; -record Model(String path, int contextSize, int maxContextSize, int gpuLayers, int threads, String promptTemplate) { +record Model(String path, int contextSize, int maxContextSize, int gpuLayers, int threads, String promptTemplate, long ctime) { } diff --git a/src/de/extio/lm_launcher/ModelPropertiesDialog.java b/src/de/extio/lm_launcher/ModelPropertiesDialog.java index 17ad9f7..e37b945 100644 --- a/src/de/extio/lm_launcher/ModelPropertiesDialog.java +++ b/src/de/extio/lm_launcher/ModelPropertiesDialog.java @@ -117,13 +117,15 @@ public class ModelPropertiesDialog extends JDialog { @Override public void actionPerformed(final ActionEvent e) { ModelPropertiesDialog.this.setVisible(false); + final Model modelToSave = editModel != null ? editModel : Data.defaultModel(modelPath); modelConsumer.accept( new Model(modelPath, Integer.parseInt(ModelPropertiesDialog.this.textField_3.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField_1.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField_2.getText()), - ModelPropertiesDialog.this.txtPrompttemplate.getText())); + ModelPropertiesDialog.this.txtPrompttemplate.getText(), + modelToSave.ctime())); ModelPropertiesDialog.this.dispose(); } });