Add model creation time and implement model tree view in UI

This commit is contained in:
extio-de
2026-04-24 21:35:21 +02:00
parent 7972c97b7f
commit e4746248dd
5 changed files with 171 additions and 123 deletions

Binary file not shown.

View File

@@ -5,6 +5,8 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@@ -45,7 +47,7 @@ public class Data {
for (int i = 0; i < modelData.models().size(); i++) { for (int i = 0; i < modelData.models().size(); i++) {
final Model model = modelData.models.get(i); final Model model = modelData.models.get(i);
if (model.maxContextSize() < model.contextSize() || model.promptTemplate() == null) { 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(); sortModels();
@@ -83,8 +85,9 @@ public class Data {
static void sortModels() { static void sortModels() {
modelData.models().sort(Comparator modelData.models().sort(Comparator
.comparing(Data::modelName, String.CASE_INSENSITIVE_ORDER) .<Model, String>comparing(m -> Path.of(m.path()).getParent().toString())
.thenComparing(Model::path)); .thenComparing(Model::ctime).reversed()
.thenComparing(Data::modelName, String.CASE_INSENSITIVE_ORDER));
} }
static void saveApps() { static void saveApps() {
@@ -103,24 +106,57 @@ public class Data {
} }
static List<String> scanModels() { static List<String> scanModels() {
final List<String> result = new ArrayList<>(); return scanModelPaths().stream().map(p -> p.toString()).toList();
}
static List<Path> scanModelPaths() {
final List<Path> result = new ArrayList<>();
try { try {
try (var paths = Files.walk(Path.of(props.getProperty("models")))) { try (var paths = Files.walk(Path.of(props.getProperty("models")))) {
paths paths
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".gguf")) .filter(p -> p.getFileName().toString().endsWith(".gguf"))
.map(Path::toAbsolutePath) .map(Path::toAbsolutePath)
.map(Path::toString) .map(Path::normalize)
.sorted()
.forEach(result::add); .forEach(result::add);
} }
} }
catch (final IOException e) { catch (final IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
// Sort by parent path (alphabetical), then by file ctime within the same directory
result.sort(Comparator
.<Path, String>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; 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<Model> models) { static record ModelData(List<Model> models) {
} }

View File

@@ -1,6 +1,7 @@
package de.extio.lm_launcher; package de.extio.lm_launcher;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component; import java.awt.Component;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.GridLayout; import java.awt.GridLayout;
@@ -9,7 +10,9 @@ import java.awt.event.ActionListener;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.swing.AbstractCellEditor; import javax.swing.AbstractCellEditor;
import javax.swing.Box; import javax.swing.Box;
@@ -27,6 +30,7 @@ import javax.swing.JScrollPane;
import javax.swing.JSlider; import javax.swing.JSlider;
import javax.swing.JSplitPane; import javax.swing.JSplitPane;
import javax.swing.JTable; import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.ListSelectionModel; import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants; import javax.swing.SwingConstants;
import javax.swing.WindowConstants; import javax.swing.WindowConstants;
@@ -34,6 +38,11 @@ import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer; 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 { public class LaunchFrame extends JFrame {
@@ -57,7 +66,7 @@ public class LaunchFrame extends JFrame {
JList list; JList list;
JList list_1; JTree modelTree;
JButton btnDelete; JButton btnDelete;
@@ -77,8 +86,6 @@ public class LaunchFrame extends JFrame {
DefaultListModel<String> listModel; DefaultListModel<String> listModel;
DefaultListModel<String> list1Model;
Component horizontalStrut; Component horizontalStrut;
JSplitPane splitPane_1; JSplitPane splitPane_1;
@@ -133,50 +140,33 @@ public class LaunchFrame extends JFrame {
{ {
this.panel = new JPanel(); this.panel = new JPanel();
this.contentPane.add(this.panel, BorderLayout.SOUTH); 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 = new JButton("Edit");
this.btnEdit.addActionListener(new ActionListener() { this.btnEdit.addActionListener(new ActionListener() {
@Override @Override
public void actionPerformed(final ActionEvent e) { public void actionPerformed(final ActionEvent e) {
final int index = LaunchFrame.this.list_1.getSelectedIndex(); final String selectedPath = LaunchFrame.this.getSelectedModelPath();
if (index == -1) { if (selectedPath == null) {
JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE);
return; return;
} }
final Model editModel = Data.modelData.models().get(index); final Model existingModel = Data.findModelByPath(selectedPath);
final ModelPropertiesDialog mpDialog = new ModelPropertiesDialog(LaunchFrame.this, editModel.path(), editModel, model -> { final Model editModel = existingModel != null ? existingModel : Data.defaultModel(selectedPath);
Data.modelData.models().set(index, model); 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 { try {
Data.saveModels(); Data.saveModels();
} }
finally { finally {
LaunchFrame.this.refresh(); LaunchFrame.this.refreshModelSliders(model);
} }
}); });
mpDialog.setLocationRelativeTo(LaunchFrame.this); mpDialog.setLocationRelativeTo(LaunchFrame.this);
@@ -185,51 +175,6 @@ public class LaunchFrame extends JFrame {
}); });
this.panel.add(this.btnEdit); 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(); this.panel_1 = new JPanel();
@@ -289,32 +234,26 @@ public class LaunchFrame extends JFrame {
this.scrollPane_1 = new JScrollPane(); this.scrollPane_1 = new JScrollPane();
this.splitPane_2.setLeftComponent(this.scrollPane_1); this.splitPane_2.setLeftComponent(this.scrollPane_1);
this.list1Model = new DefaultListModel<>();
this.list_1 = new JList(this.list1Model); // Tree view for models
this.list_1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); this.modelTree = new JTree(new DefaultMutableTreeNode("Models"));
this.list_1.getSelectionModel().addListSelectionListener(e -> { this.modelTree.setRootVisible(false);
if (!e.getValueIsAdjusting()) { this.modelTree.setShowsRootHandles(true);
final DefaultListSelectionModel selectionModel = (DefaultListSelectionModel) e.getSource(); this.modelTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
if (selectionModel.getMinSelectionIndex() > -1) { this.modelTree.setCellRenderer(new ModelTreeCellRenderer());
final Model model = Data.modelData.models().get(selectionModel.getMinSelectionIndex()); // Set smaller font for tree to account for UI scaling
this.modelTree.setFont(this.modelTree.getFont().deriveFont(10f));
this.slider_context.setMaximum(model.maxContextSize()); this.modelTree.addTreeSelectionListener(e -> {
this.slider_context.setMajorTickSpacing(model.maxContextSize() / 4); final DefaultMutableTreeNode node = (DefaultMutableTreeNode) this.modelTree.getLastSelectedPathComponent();
this.slider_context.setMinorTickSpacing(model.maxContextSize() / 16); if (node != null && node.isLeaf() && node.getUserObject() instanceof Path) {
this.slider_context.setValue(model.contextSize()); final Path p = (Path) node.getUserObject();
this.slider_context.setLabelTable(this.slider_context.createStandardLabels(model.maxContextSize() / 4)); final Model saved = Data.findModelByPath(p.toString());
this.slider_context.setSnapToTicks(true); final Model model = saved != null ? saved : Data.defaultModel(p.toString());
this.slider_context.repaint(); this.refreshModelSliders(model);
this.slider_threads.setValue(model.threads());
this.slider_gpu.setValue(model.gpuLayers());
this.lblPromptTemplate_1.setText(model.promptTemplate());
}
} }
}); });
this.scrollPane_1.setViewportView(this.list_1); this.scrollPane_1.setViewportView(this.modelTree);
{ {
this.panel_3 = new JPanel(); this.panel_3 = new JPanel();
this.splitPane_2.setRightComponent(this.panel_3); this.splitPane_2.setRightComponent(this.panel_3);
@@ -498,16 +437,18 @@ public class LaunchFrame extends JFrame {
final App app = Data.appData.apps().get(AppIndex); final App app = Data.appData.apps().get(AppIndex);
Model model = null; Model model = null;
final int modelIndex = LaunchFrame.this.list_1.getSelectedIndex(); final String selectedPath = LaunchFrame.this.getSelectedModelPath();
if (modelIndex > -1) { if (selectedPath != null) {
final Model template = Data.modelData.models().get(modelIndex); final Model saved = Data.findModelByPath(selectedPath);
final Model template = saved != null ? saved : Data.defaultModel(selectedPath);
model = new Model( model = new Model(
template.path(), template.path(),
LaunchFrame.this.slider_context.getValue(), LaunchFrame.this.slider_context.getValue(),
template.maxContextSize(), template.maxContextSize(),
LaunchFrame.this.slider_gpu.getValue(), LaunchFrame.this.slider_gpu.getValue(),
LaunchFrame.this.slider_threads.getValue(), LaunchFrame.this.slider_threads.getValue(),
template.promptTemplate()); template.promptTemplate(),
template.ctime());
} }
final List<AppArgument> argumentOverrides = new ArrayList<>(); final List<AppArgument> argumentOverrides = new ArrayList<>();
@@ -550,28 +491,97 @@ public class LaunchFrame extends JFrame {
this.refresh(); 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() { private void refresh() {
Data.sortModels(); // Rebuild apps list
this.listModel.clear(); this.listModel.clear();
Data.appData.apps() Data.appData.apps()
.stream() .stream()
.map(app -> app.path().getFileName() + " " + app.path().getParent() + " " + app.interpreter()) .map(app -> app.path().getFileName() + " " + app.path().getParent() + " " + app.interpreter())
.forEach(this.listModel::addElement); .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) { if (this.table.getCellEditor() != null) {
this.table.getCellEditor().stopCellEditing(); this.table.getCellEditor().stopCellEditing();
} }
this.appArgumentsTableModel.setRowCount(0); 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<Path, DefaultMutableTreeNode> 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(); 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 { static class AppArgumentsJCheckBoxRenderer extends JCheckBox implements TableCellRenderer {
private final DefaultTableModel model; private final DefaultTableModel model;
@@ -617,4 +627,4 @@ public class LaunchFrame extends JFrame {
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package de.extio.lm_launcher; 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) {
} }

View File

@@ -117,13 +117,15 @@ public class ModelPropertiesDialog extends JDialog {
@Override @Override
public void actionPerformed(final ActionEvent e) { public void actionPerformed(final ActionEvent e) {
ModelPropertiesDialog.this.setVisible(false); ModelPropertiesDialog.this.setVisible(false);
final Model modelToSave = editModel != null ? editModel : Data.defaultModel(modelPath);
modelConsumer.accept( modelConsumer.accept(
new Model(modelPath, new Model(modelPath,
Integer.parseInt(ModelPropertiesDialog.this.textField_3.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField_3.getText()),
Integer.parseInt(ModelPropertiesDialog.this.textField.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField.getText()),
Integer.parseInt(ModelPropertiesDialog.this.textField_1.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField_1.getText()),
Integer.parseInt(ModelPropertiesDialog.this.textField_2.getText()), Integer.parseInt(ModelPropertiesDialog.this.textField_2.getText()),
ModelPropertiesDialog.this.txtPrompttemplate.getText())); ModelPropertiesDialog.this.txtPrompttemplate.getText(),
modelToSave.ctime()));
ModelPropertiesDialog.this.dispose(); ModelPropertiesDialog.this.dispose();
} }
}); });