Done some basic things

This commit is contained in:
2026-03-23 08:14:23 +01:00
parent 2a2405ceae
commit 3292c5bfbe
22 changed files with 978 additions and 349 deletions

29
pom.xml
View File

@@ -1,13 +1,16 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0"
<modelVersion>4.0.0</modelVersion> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<groupId>de.derpandaa</groupId> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>plinfa</artifactId> <modelVersion>4.0.0</modelVersion>
<version>0.0.1-SNAPSHOT</version> <groupId>de.derpandaa</groupId>
<properties> <artifactId>plinfa</artifactId>
<qtjambi.version>6.10.0</qtjambi.version> <version>0.0.1-SNAPSHOT</version>
<properties>
<qtjambi.version>6.11.0</qtjambi.version>
<jackson.version>3.0.3</jackson.version> <jackson.version>3.0.3</jackson.version>
<maven.compiler.release>25</maven.compiler.release>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>io.qtjambi</groupId> <groupId>io.qtjambi</groupId>
<artifactId>qtjambi</artifactId> <artifactId>qtjambi</artifactId>
@@ -18,6 +21,16 @@
<artifactId>qtjambi-native-linux-x64</artifactId> <artifactId>qtjambi-native-linux-x64</artifactId>
<version>${qtjambi.version}</version> <version>${qtjambi.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf-extras</artifactId>
<version>3.4.1</version>
</dependency>
<dependency> <dependency>
<groupId>tools.jackson.core</groupId> <groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId> <artifactId>jackson-core</artifactId>

View File

@@ -1,71 +0,0 @@
package de.derpandaa.aitestsuit;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.function.Consumer;
import de.derpandaa.aitestsuit.MessageDto.Role;
import de.derpandaa.aitestsuit.OpenAiDto.Model;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
/*
* I dont know how to name it
*/
public class ChatGPTApiWrapper {
public static String API_KEY;
public static String run(String systemPrompt, String userPrompt) {
OpenAiDto dto = new OpenAiDto(Model.gpt5nano, new MessageDto(Role.system, systemPrompt),
new MessageDto(Role.user, userPrompt));
return run(dto);
}
public static String run(OpenAiDto dto) {
ObjectMapper mapper = new ObjectMapper();
String jsonBody = mapper.writeValueAsString(dto);
System.out.println(jsonBody);
HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
HttpClient client = HttpClient.newHttpClient();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode root = mapper.readTree(response.body());
return root.path("choices").path(0).path("message").path("content").asString();
} catch (IOException | InterruptedException e) {
// NOOP
}
return "";
}
public static void runAsync(OpenAiDto dto, Consumer<String> onResult) {
ObjectMapper mapper = new ObjectMapper();
String jsonBody = mapper.writeValueAsString(dto);
HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
HttpClient client = HttpClient.newHttpClient();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body)
.thenAccept(body -> {
try {
JsonNode root = mapper.readTree(body);
String content = root.path("choices").path(0).path("message").path("content").asString();
onResult.accept(content);
} catch (Exception e) {
onResult.accept("");
}
}).exceptionally(ex -> {
onResult.accept("");
return null;
});
}
}

View File

@@ -1,206 +0,0 @@
package de.derpandaa.aitestsuit;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
import de.derpandaa.aitestsuit.MessageDto.Role;
import de.derpandaa.aitestsuit.OpenAiDto.Model;
import io.qt.core.QMetaObject;
import io.qt.core.QMimeData;
import io.qt.core.Qt.AlignmentFlag;
import io.qt.core.Qt.ConnectionType;
import io.qt.gui.QColor;
import io.qt.gui.QKeySequence;
import io.qt.gui.QPalette;
import io.qt.gui.QShortcut;
import io.qt.widgets.QApplication;
import io.qt.widgets.QBoxLayout;
import io.qt.widgets.QComboBox;
import io.qt.widgets.QFrame;
import io.qt.widgets.QHBoxLayout;
import io.qt.widgets.QLabel;
import io.qt.widgets.QMainWindow;
import io.qt.widgets.QPushButton;
import io.qt.widgets.QScrollArea;
import io.qt.widgets.QSizePolicy;
import io.qt.widgets.QSizePolicy.Policy;
import io.qt.widgets.QTextEdit;
import io.qt.widgets.QVBoxLayout;
import io.qt.widgets.QWidget;
public class ChatWindow extends QMainWindow {
private QBoxLayout layout;
private QVBoxLayout chatLayout;
private QWidget chatWidget;
private QTextEdit textInput;
private OpenAiDto openAiDto;
private QComboBox modelComboBox;
public ChatWindow() {
super();
openAiDto = new OpenAiDto(Model.gpt5_2, new MessageDto(Role.system, "Antworte kurz und präzise"));
QWidget widget = new QWidget();
layout = new QVBoxLayout();
layout.setContentsMargins(10, 10, 10, 10);
QPushButton button = new QPushButton("Import Chat");
layout.addWidget(button);
modelComboBox = new QComboBox();
for (OpenAiDto.Model m : OpenAiDto.Model.values()) {
modelComboBox.addItem(m.toString(), m);
}
layout.addWidget(modelComboBox);
QScrollArea scrollArea = new QScrollArea();
scrollArea.setWidgetResizable(true);
chatWidget = new QWidget();
chatLayout = new QVBoxLayout(chatWidget);
chatLayout.setAlignment(AlignmentFlag.AlignTop);
chatLayout.setSpacing(5);
scrollArea.setWidget(chatWidget);
layout.addWidget(scrollArea);
QHBoxLayout inputLayout = new QHBoxLayout();
textInput = new QTextEdit() {
@Override
protected void insertFromMimeData(QMimeData source) {
insertPlainText(source.text());
}
};
textInput.setMaximumHeight(100);
textInput.setPlaceholderText("Nachricht eingeben...");
QPushButton sendButton = new QPushButton("Senden");
sendButton.clicked.connect(() -> sendMessage());
sendButton.setSizePolicy(new QSizePolicy(Policy.Minimum, Policy.Minimum));
inputLayout.addWidget(textInput);
inputLayout.addWidget(sendButton);
layout.addLayout(inputLayout);
QShortcut shortcut = new QShortcut(new QKeySequence("Ctrl+Return"), widget);
shortcut.activated.connect(() -> sendMessage());
widget.setLayout(layout);
setCentralWidget(widget);
resize(600, 500);
show();
}
private void importChat(OpenAiDto openAiDto) {
for (MessageDto messageDto : openAiDto.getMessages()) {
// chatLayout.addWidget(createMessage(messageDto.getContent(), true));
}
}
private QWidget createMessage(String message, Role role) {
QFrame frame = new QFrame();
frame.setFrameShape(QFrame.Shape.Box);
frame.setFrameShadow(QFrame.Shadow.Raised);
// frame.setMaximumWidth(Integer.MAX_VALUE);
frame.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred);
QVBoxLayout frameLayout = new QVBoxLayout(frame);
frameLayout.setContentsMargins(10, 10, 10, 10);
QLabel textLabel = new QLabel(message);
textLabel.setWordWrap(true);
frameLayout.addWidget(textLabel);
QWidget container = new QWidget();
QHBoxLayout boxLayout = new QHBoxLayout(container);
boxLayout.setContentsMargins(0, 0, 0, 0);
QPalette palette = frame.palette();
QColor messageColor;
switch (role) {
case user:
messageColor = new QColor(209, 114, 246);
boxLayout.addStretch(30);
boxLayout.addWidget(frame, 70);
break;
case assistant:
messageColor = new QColor(106, 91, 117);
boxLayout.addWidget(frame, 70);
boxLayout.addStretch(30);
break;
default:
messageColor = new QColor(100, 100, 100);
boxLayout.addStretch(15);
boxLayout.addWidget(frame, 70);
boxLayout.addStretch(15);
break;
}
palette.setColor(QPalette.ColorRole.Window, messageColor);
frame.setAutoFillBackground(true);
frame.setPalette(palette);
return container;
}
private void sendMessage() {
String message = textInput.getPlainText().trim();
if (message.isEmpty()) {
return;
}
openAiDto.setModel((Model) modelComboBox.currentData());
MessageDto messageDto = new MessageDto(Role.user, message);
openAiDto.addMessage(messageDto);
chatLayout.addWidget(createMessage(message, Role.user));
textInput.clear();
chatWidget.adjustSize();
QWidget loadingWidget = createMessage("Loading...", Role.assistant);
chatLayout.addWidget(loadingWidget);
chatWidget.adjustSize();
ChatGPTApiWrapper.runAsync(openAiDto, answer -> {
QMetaObject.invokeMethod(this, () -> {
chatLayout.removeWidget(loadingWidget);
loadingWidget.disposeLater();
openAiDto.addMessage(new MessageDto(Role.assistant, answer));
chatLayout.addWidget(createMessage(answer, Role.assistant));
chatWidget.adjustSize();
}, ConnectionType.QueuedConnection);
// update();
});
}
public static void main(String[] args) {
QApplication.initialize(args);
QApplication.setStyle("Fusion");
initProperties();
ChatWindow window = new ChatWindow();
QApplication.exec();
QApplication.shutdown();
}
private static void initProperties() {
Properties properties = new Properties();
try {
properties.load(new FileInputStream("ai.properties"));
} catch (FileNotFoundException e) {
System.out.println("[ERROR] No Properties File");
System.exit(1);
} catch (IOException e) {
System.out.println("[ERROR] Cannot Access File");
System.exit(1);
}
ChatGPTApiWrapper.API_KEY = properties.getProperty("OPENAI_KEY");
}
}

View File

@@ -1,52 +0,0 @@
package de.derpandaa.aitestsuit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OpenAiDto {
public enum Model {
gpt5nano("gpt-5-nano"),
gpt5mini("gpt-5-mini"),
gpt5_2("gpt-5.2");
final String value;
Model(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
@JsonProperty("model")
private Model model;
@JsonProperty("messages")
private List<MessageDto> messages;
public OpenAiDto(Model model, MessageDto... messages) {
this.model = model;
this.messages = new ArrayList<MessageDto>();
this.messages.addAll(Arrays.asList(messages));
}
public void addMessage(MessageDto messageDto) {
messages.add(messageDto);
}
public List<MessageDto> getMessages() {
return messages;
}
public void setModel(Model model) {
this.model = model;
}
}

View File

@@ -1,4 +1,4 @@
package de.derpandaa.aitestsuit; package de.derpandaa.plinfa;
import io.qt.widgets.QApplication; import io.qt.widgets.QApplication;
import io.qt.widgets.QBoxLayout; import io.qt.widgets.QBoxLayout;
@@ -9,12 +9,9 @@ import io.qt.widgets.QTextEdit;
import io.qt.widgets.QVBoxLayout; import io.qt.widgets.QVBoxLayout;
import io.qt.widgets.QWidget; import io.qt.widgets.QWidget;
public class AiWindow { public class AiWindow extends QMainWindow {
public AiWindow() {
super();
public static void main(String[] args) {
QApplication.initialize(args);
QMainWindow window = new QMainWindow();
QWidget widget = new QWidget(); QWidget widget = new QWidget();
QBoxLayout layout = new QVBoxLayout(); QBoxLayout layout = new QVBoxLayout();
@@ -39,16 +36,23 @@ public class AiWindow {
QPushButton buttonRun = new QPushButton("Run"); QPushButton buttonRun = new QPushButton("Run");
buttonRun.clicked.connect(() -> { buttonRun.clicked.connect(() -> {
String result = ChatGPTApiWrapper.run(editSystemPrompt.getPlainText(), editUserPrompt.getPlainText()); // String result = AiProviderManager.getAiProviderManager().run(editSystemPrompt.getPlainText(),
editAiAnser.setText(result); // editUserPrompt.getPlainText());
// editAiAnser.setText(result);
}); });
layout.addWidget(buttonRun); layout.addWidget(buttonRun);
widget.setLayout(layout); widget.setLayout(layout);
window.setCentralWidget(widget); setCentralWidget(widget);
window.show(); show();
}
public static void main(String[] args) {
QApplication.initialize(args);
AiWindow window = new AiWindow();
QApplication.exec(); QApplication.exec();
QApplication.shutdown(); QApplication.shutdown();
} }

View File

@@ -0,0 +1,67 @@
package de.derpandaa.plinfa;
import java.awt.event.FocusEvent;
import java.io.File;
import java.util.function.Consumer;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import de.derpandaa.plinfa.dto.ChatDto;
import de.derpandaa.plinfa.projects.GenericProject;
public class ChatListWidget extends JTree {
private static final long serialVersionUID = 559712098635547536L;
private final Consumer<ChatDto> clickHandler;
public ChatListWidget(Consumer<ChatDto> clickedFunction) {
super();
this.clickHandler = clickedFunction;
setRootVisible(true);
GenericProject rootProject = new GenericProject(new File(ChatDto.PROJECT_DIR));
DefaultMutableTreeNode rootNode = buildTree(rootProject);
setModel(new DefaultTreeModel(rootNode));
addTreeSelectionListener(e -> {
if (getLastSelectedPathComponent() == null)
return;
Object node = ((DefaultMutableTreeNode) getLastSelectedPathComponent()).getUserObject();
if (node instanceof ChatDto chat) {
clickHandler.accept(chat);
}
});
}
private DefaultMutableTreeNode buildTree(GenericProject project) {
DefaultMutableTreeNode node = new DefaultMutableTreeNode(project.getName());
for (GenericProject sub : project.getSubProjects()) {
node.add(buildTree(sub));
}
for (ChatDto chat : project.getChats()) {
DefaultMutableTreeNode chatNode = new DefaultMutableTreeNode(chat);
node.add(chatNode);
System.out.println(chat.getName());
}
return node;
}
public void addChat(ChatDto chat) {
DefaultMutableTreeNode root = (DefaultMutableTreeNode) getModel().getRoot();
DefaultMutableTreeNode chatNode = new DefaultMutableTreeNode(chat);
root.add(chatNode);
((DefaultTreeModel) getModel()).reload(root);
}
@Override
protected void processFocusEvent(FocusEvent e) {
if (e.FOCUS_LOST == e.getID()) {
clearSelection();
return;
}
super.processFocusEvent(e);
}
}

View File

@@ -0,0 +1,191 @@
package de.derpandaa.plinfa;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import de.derpandaa.plinfa.aiprovider.AiProviderManager;
import de.derpandaa.plinfa.aiprovider.Model;
import de.derpandaa.plinfa.dto.AiRequest;
import de.derpandaa.plinfa.dto.ChatDto;
import de.derpandaa.plinfa.dto.MessageDto;
import de.derpandaa.plinfa.dto.MessageDto.Role;
public class ChatWindow extends JPanel {
private static final long serialVersionUID = -1698148372733277109L;
private final JPanel messageContainer;
private final JScrollPane scrollPane;
private final JTextArea input;
private JComboBox<Model> modelComboBox;
private ChatDto chatDto;
private final Runnable onFirstSave;
public ChatWindow(Runnable onFirstSave) {
this.onFirstSave = onFirstSave;
setLayout(new BorderLayout());
modelComboBox = new JComboBox<>();
for (Model m : AiProviderManager.getAiProviderManager().getActiveModels()) {
modelComboBox.addItem(m);
}
add(modelComboBox, BorderLayout.NORTH);
// ===== Message Area =====
messageContainer = new JPanel();
messageContainer.setLayout(new BoxLayout(messageContainer, BoxLayout.Y_AXIS));
messageContainer.setAlignmentY(Component.TOP_ALIGNMENT);
messageContainer.setOpaque(false);
JPanel northWrapper = new JPanel(new BorderLayout());
northWrapper.add(messageContainer, BorderLayout.NORTH);
scrollPane = new JScrollPane(northWrapper);
scrollPane.setBorder(null);
scrollPane.setBorder(BorderFactory.createLineBorder(PlinfaWindow.BG_ALT, 2));
scrollPane.getVerticalScrollBar().setUnitIncrement(12);
add(scrollPane, BorderLayout.CENTER);
// ===== Input Area =====
JPanel inputPanel = new JPanel(new BorderLayout());
input = new JTextArea(3, 20);
input.setLineWrap(true);
input.setWrapStyleWord(true);
JButton send = new JButton("Send");
send.addActionListener(e -> sendMessage());
inputPanel.add(new JScrollPane(input), BorderLayout.CENTER);
inputPanel.add(send, BorderLayout.EAST);
add(inputPanel, BorderLayout.SOUTH);
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK),
"save");
getActionMap().put("save", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
saveChat();
}
});
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK),
"send");
getActionMap().put("send", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
sendMessage();
}
});
chatDto = new ChatDto();
}
public MessageBubble addUserMessage(String text) {
return addMessage(new MessageDto(Role.user, text));
}
public MessageBubble addAssistantMessage(String text) {
return addMessage(new MessageDto(Role.assistant, text));
}
private MessageBubble addMessage(MessageDto messageDto) {
MessageBubble bubble = new MessageBubble(messageDto);
messageContainer.add(bubble);
messageContainer.revalidate();
messageContainer.repaint();
scrollToBottom();
return bubble;
}
private void scrollToBottom() {
SwingUtilities.invokeLater(() -> {
messageContainer.revalidate();
messageContainer.repaint();
SwingUtilities.invokeLater(() -> {
JScrollBar bar = scrollPane.getVerticalScrollBar();
bar.setValue(bar.getMaximum());
});
});
}
private void sendMessage() {
String text = input.getText().trim();
if (text.isEmpty())
return;
addUserMessage(text);
input.setText("");
AiRequest request = new AiRequest((Model) modelComboBox.getSelectedItem());
request.addMessages(new MessageDto(Role.user, text));
request.setSystemPrompt("Du bist ein einfacher Assistent der kurz antwortet");
MessageBubble bubble = addAssistantMessage("Loading...");
AiProviderManager.getAiProviderManager().runAsync(request, answer -> {
SwingUtilities.invokeLater(() -> {
addAssistantMessage(answer);
chatDto.addMessage(new MessageDto(Role.user, text));
chatDto.addMessage(new MessageDto(Role.assistant, answer));
messageContainer.remove(bubble);
messageContainer.revalidate();
messageContainer.repaint();
chatDto.writeToFile();
});
});
}
public void loadChat(ChatDto chatDto) {
this.chatDto = chatDto == null ? new ChatDto() : chatDto;
chatDto.getMessages().forEach(m -> {
if (m.getRole() == Role.user) {
addUserMessage(m.getContent());
} else if (m.getRole() == Role.assistant) {
addAssistantMessage(m.getContent());
}
});
}
private void saveChat() {
if (chatDto.getName() == null) {
String name = JOptionPane.showInputDialog(this, "Chat Name:", "Speichern", JOptionPane.PLAIN_MESSAGE);
if (name == null || name.isBlank())
return;
chatDto.setName(name);
chatDto.writeToFile();
onFirstSave.run();
}
}
public void focusInput() {
input.requestFocusInWindow();
}
}

View File

@@ -0,0 +1,59 @@
package de.derpandaa.plinfa;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.border.EmptyBorder;
import de.derpandaa.plinfa.dto.MessageDto;
public class MessageBubble extends JPanel {
private static final long serialVersionUID = -6278277156331505629L;
public MessageBubble(MessageDto messageDto) {
setLayout(new BorderLayout());
setBorder(new EmptyBorder(6, 10, 6, 10));
JTextArea label = new JTextArea(messageDto.getContent());
label.setLineWrap(true);
label.setWrapStyleWord(true);
label.setEditable(false);
label.setOpaque(false);
JPanel bubble = new JPanel(new BorderLayout());
bubble.setBorder(new EmptyBorder(10, 12, 10, 12));
bubble.add(label, BorderLayout.CENTER);
Color bg;
FlowLayout wrapperLayout;
switch (messageDto.getRole()) {
case user -> {
bg = new Color(120, 80, 200);
wrapperLayout = new FlowLayout(FlowLayout.RIGHT);
}
case assistant -> {
bg = new Color(60, 60, 60);
wrapperLayout = new FlowLayout(FlowLayout.LEFT);
}
default -> {
bg = new Color(90, 90, 90);
wrapperLayout = new FlowLayout(FlowLayout.CENTER);
}
}
bubble.setBackground(bg);
bubble.setOpaque(true);
JPanel wrapper = new JPanel(wrapperLayout);
wrapper.setOpaque(false);
wrapper.add(bubble);
setOpaque(false);
add(wrapper, BorderLayout.CENTER);
}
}

View File

@@ -0,0 +1,47 @@
package de.derpandaa.plinfa;
import java.awt.Dimension;
import java.util.function.Consumer;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
public class PlinfaMenu extends JToolBar {
public PlinfaMenu() {
super();
setFloatable(false);
setPreferredSize(new Dimension(0, 50));
}
public void addToggleButton(Icon icon, Consumer<Boolean> function, String toolTip) {
JToggleButton button = new JToggleButton();
button.setIcon(icon);
button.setToolTipText(toolTip);
button.setSelected(true);
button.addActionListener(e -> {
if (function != null) {
function.accept(button.isSelected());
}
});
add(button);
}
public void addButton(Icon icon, Runnable function, String toolTip) {
JButton button = new JButton();
button.setIcon(icon);
button.setToolTipText(toolTip);
button.addActionListener(e -> {
if (function != null)
function.run();
});
add(button);
}
}

View File

@@ -0,0 +1,141 @@
package de.derpandaa.plinfa;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter;
import de.derpandaa.plinfa.aiprovider.OpenAiProvider;
import de.derpandaa.plinfa.dto.ChatDto;
public class PlinfaWindow extends JFrame {
private static final long serialVersionUID = 1340010605617450301L;
public static final Color BG = new Color(40, 42, 46);
public static final Color BG_ALT = new Color(55, 59, 65);
public static final Color FG = new Color(208, 208, 208);
public static final Color ACCENT = new Color(0x6A5B75);
private final Map<ChatDto, ChatWindow> chatCache = new HashMap<>();
private ChatWindow currentChatWindow;
private JPanel centerContainer;
private ChatListWidget chatListWidget;
public PlinfaWindow() {
super("Plinfa");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1200, 800);
setLocationRelativeTo(null);
applyTheme();
initUI();
setVisible(true);
}
private void initUI() {
setLayout(new BorderLayout());
PlinfaMenu menu = new PlinfaMenu();
FlatSVGIcon addIcon = new FlatSVGIcon("img/add-icon.svg");
addIcon.setColorFilter(new ColorFilter((color) -> PlinfaWindow.ACCENT));
menu.addButton(addIcon, () -> {
loadChat(new ChatDto());
}, "Neuer Chat");
add(menu, BorderLayout.NORTH);
chatListWidget = new ChatListWidget(this::loadChat);
JScrollPane leftScroll = new JScrollPane(chatListWidget);
leftScroll.setFocusable(false);
leftScroll.setPreferredSize(new Dimension(260, 0));
centerContainer = new JPanel(new BorderLayout());
loadChat(new ChatDto());
centerContainer.add(currentChatWindow, BorderLayout.CENTER);
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroll, centerContainer);
splitPane.setDividerLocation(260);
splitPane.setResizeWeight(0.0);
splitPane.setBorder(new javax.swing.border.EmptyBorder(10, 10, 10, 10));
add(splitPane, BorderLayout.CENTER);
}
private void applyTheme() {
UIManager.put("Panel.background", BG);
UIManager.put("OptionPane.background", BG);
UIManager.put("Label.foreground", FG);
UIManager.put("TextField.background", BG_ALT);
}
public void loadChat(ChatDto chatDto) {
centerContainer.removeAll();
ChatWindow window = chatCache.get(chatDto);
if (window == null) {
window = new ChatWindow(() -> {
chatListWidget.addChat(chatDto);
});
window.loadChat(chatDto);
chatCache.put(chatDto, window);
}
currentChatWindow = window;
centerContainer.add(currentChatWindow, BorderLayout.CENTER);
centerContainer.revalidate();
centerContainer.repaint();
currentChatWindow.focusInput();
}
public static void main(String[] args) {
initProperties();
try {
FlatDarkLaf.setup();
} catch (Exception e) {
e.printStackTrace();
}
UIManager.put("Component.focusColor", new Color(40, 42, 46));
SwingUtilities.invokeLater(PlinfaWindow::new);
}
private static void initProperties() {
Properties props = new Properties();
try {
props.load(new FileInputStream("ai.properties"));
} catch (IOException e) {
System.out.println("[ERROR] Cannot load config");
System.exit(1);
}
OpenAiProvider.API_KEY = props.getProperty("OPENAI_KEY");
ChatDto.PROJECT_DIR = props.getProperty("PROJECT_DIR");
}
public static void noOutline(JComponent c) {
c.putClientProperty("focusWidth", 0);
}
}

View File

@@ -0,0 +1,69 @@
package de.derpandaa.plinfa.aiprovider;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.function.Consumer;
import javax.swing.JOptionPane;
import de.derpandaa.plinfa.dto.AiRequest;
import de.derpandaa.plinfa.dto.AiResponseDto;
import tools.jackson.databind.ObjectMapper;
public abstract class AbstractAiProvider {
protected static final ObjectMapper mapper = new ObjectMapper();
public static String OpenAiKey;
// GPT5_NANO("gpt-5-nano", Provider.OPENAI), GPT5_MINI("gpt-5-mini", Provider.OPENAI),
// GPT5_2("gpt-5.2", Provider.OPENAI), CLAUDE_OPUS("claude-opus-4-6", Provider.ANTHROPIC),
// CLAUDE_SONNET("claude-sonnet-4-6", Provider.ANTHROPIC), CLAUDE_HAIKU("claude-haiku-4-5", Provider.ANTHROPIC);
public abstract List<Model> getModels();
public abstract HttpRequest buildRequest(String jsonBody);
public abstract AiResponseDto parseResponse(String body);
public AiResponseDto run(AiRequest request) {
String jsonBody = mapper.writeValueAsString(request);
System.out.println(jsonBody);
HttpRequest httpRequest = buildRequest(jsonBody);
HttpClient client = HttpClient.newHttpClient();
try {
HttpResponse<String> response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());
return parseResponse(response.body());
} catch (IOException | InterruptedException e) {
// NOOP
}
return null;
}
public void runAsync(AiRequest request, Consumer<String> onResult) {
try {
String jsonBody = buildRequestBody(request);
HttpRequest httpRequest = buildRequest(jsonBody);
HttpClient client = HttpClient.newHttpClient();
client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body)
.thenAccept(body -> {
AiResponseDto dto = parseResponse(body);
onResult.accept(dto != null ? dto.getResponse() : "");
});
} catch (Exception e) {
JOptionPane.showMessageDialog(null, e.getMessage());
onResult.accept("");
}
}
public abstract String buildRequestBody(AiRequest request) throws Exception;
}

View File

@@ -0,0 +1,50 @@
package de.derpandaa.plinfa.aiprovider;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import de.derpandaa.plinfa.dto.AiRequest;
public class AiProviderManager {
private final List<AbstractAiProvider> providers;
private static AiProviderManager aiProviderManager;
private List<Model> models = new ArrayList<Model>();
public AiProviderManager() {
providers = new ArrayList<AbstractAiProvider>();
if (OpenAiProvider.API_KEY != null) {
providers.add(new OpenAiProvider());
}
}
public static AiProviderManager getAiProviderManager() {
if (aiProviderManager == null) {
aiProviderManager = new AiProviderManager();
}
return aiProviderManager;
}
public List<Model> getActiveModels() {
if (models.isEmpty()) {
for (AbstractAiProvider aiProvider : providers) {
models.addAll(aiProvider.getModels());
}
}
return models;
}
public AbstractAiProvider getProviderForModel(Model model) {
for (AbstractAiProvider aiProvider : providers) {
if (model.clazz().equals(aiProvider.getClass())) {
return aiProvider;
}
}
return null;
}
public void runAsync(AiRequest aiRequest, Consumer<String> onResult) {
getProviderForModel(aiRequest.getModel()).runAsync(aiRequest, onResult);
}
}

View File

@@ -0,0 +1,16 @@
package de.derpandaa.plinfa.aiprovider;
import com.fasterxml.jackson.annotation.JsonValue;
public record Model(String modelName, Class<? extends AbstractAiProvider> clazz) {
@Override
public String toString() {
return modelName;
}
@JsonValue
public String modelName() {
return modelName;
}
}

View File

@@ -0,0 +1,67 @@
package de.derpandaa.plinfa.aiprovider;
import java.net.URI;
import java.net.http.HttpRequest;
import java.util.List;
import de.derpandaa.plinfa.dto.AiRequest;
import de.derpandaa.plinfa.dto.AiResponseDto;
import de.derpandaa.plinfa.dto.MessageDto;
import de.derpandaa.plinfa.dto.OpenAiResponseDto;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
public class OpenAiProvider extends AbstractAiProvider {
public static String API_KEY;
private static int maxTokens;
private static OpenAiProvider openAiProvider;
public OpenAiProvider() {
}
@Override
public HttpRequest buildRequest(String jsonBody) {
return HttpRequest.newBuilder().uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
}
@Override
public List<Model> getModels() {
return List.of(new Model("gpt-5-nano", this.getClass()));
}
@Override
public AiResponseDto parseResponse(String body) {
return new OpenAiResponseDto(body);
}
@Override
public String buildRequestBody(AiRequest request) throws Exception {
ObjectNode root = mapper.createObjectNode();
root.put("model", request.getModel().modelName());
root.put("max_completion_tokens", request.getMaxTokens());
if (request.getSystemPrompt() != null) {
ArrayNode messages = root.putArray("messages");
ObjectNode system = mapper.createObjectNode();
system.put("role", "system");
system.put("content", request.getSystemPrompt());
messages.add(system);
for (MessageDto m : request.getMessages()) {
ObjectNode msg = mapper.createObjectNode();
msg.put("role", m.getRole().name());
msg.put("content", m.getContent());
messages.add(msg);
}
}
return mapper.writeValueAsString(root);
}
}

View File

@@ -0,0 +1,51 @@
package de.derpandaa.plinfa.dto;
import java.util.ArrayList;
import java.util.List;
import de.derpandaa.plinfa.aiprovider.Model;
public class AiRequest {
private Model model;
private int maxTokens;
private String systemPrompt;
private List<MessageDto> messages;
public AiRequest(Model model) {
this.model = model;
this.maxTokens = 4096;
this.messages = new ArrayList<>();
}
public Model getModel() {
return model;
}
public void setModel(Model model) {
this.model = model;
}
public int getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(int maxTokens) {
this.maxTokens = maxTokens;
}
public String getSystemPrompt() {
return systemPrompt;
}
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public List<MessageDto> getMessages() {
return messages;
}
public void addMessages(MessageDto message) {
messages.add(message);
}
}

View File

@@ -0,0 +1,23 @@
package de.derpandaa.plinfa.dto;
import tools.jackson.databind.ObjectMapper;
public abstract class AiResponseDto {
protected String response;
protected int tokensUsed;
protected ObjectMapper mapper = new ObjectMapper();
public AiResponseDto(String jsonBody) {
parseBody(jsonBody);
}
protected abstract void parseBody(String jsonBody);
public String getResponse() {
return response;
}
public int getTokensUsed() {
return tokensUsed;
}
}

View File

@@ -0,0 +1,17 @@
package de.derpandaa.plinfa.dto;
import tools.jackson.databind.JsonNode;
public class AnthropicResponseDto extends AiResponseDto {
public AnthropicResponseDto(String jsonBody) {
super(jsonBody);
}
@Override
protected void parseBody(String jsonBody) {
JsonNode root = mapper.readTree(jsonBody);
response = root.path("content").path(0).path("text").asString();
tokensUsed = root.path("usage").path("input_tokens").asInt() + root.path("usage").path("output_tokens").asInt();
}
}

View File

@@ -0,0 +1,68 @@
package de.derpandaa.plinfa.dto;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
public class ChatDto {
public static String PROJECT_DIR;
private String name;
List<MessageDto> messages;
public ChatDto() {
messages = new ArrayList<MessageDto>();
}
public ChatDto(File f) {
loadFromFile(f);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<MessageDto> getMessages() {
return messages;
}
public void addMessage(MessageDto messageDto) {
messages.add(messageDto);
}
private void loadFromFile(File f) {
ObjectMapper mapper = new ObjectMapper();
setName(f.getName().substring(0, f.getName().indexOf(".json")));
try {
messages = mapper.readValue(f, new TypeReference<List<MessageDto>>() {
});
} catch (JacksonException e) {
e.printStackTrace();
System.err.println("Deserialisierung fehlgeschlagen");
}
}
public void writeToFile() {
if (name == null) {
return;
}
ObjectMapper mapper = new ObjectMapper();
File f = new File(String.format("%s/%s.json", PROJECT_DIR, name));
mapper.writeValue(f.getAbsoluteFile(), messages);
}
@Override
public String toString() {
return name;
}
}

View File

@@ -1,7 +1,9 @@
package de.derpandaa.aitestsuit; package de.derpandaa.plinfa.dto;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder({ "role", "content" })
public class MessageDto { public class MessageDto {
public enum Role { public enum Role {
@@ -13,6 +15,9 @@ public class MessageDto {
@JsonProperty("content") @JsonProperty("content")
private String content; private String content;
public MessageDto() {
}
public MessageDto(Role role, String content) { public MessageDto(Role role, String content) {
this.role = role; this.role = role;
this.content = content; this.content = content;
@@ -22,4 +27,8 @@ public class MessageDto {
return content; return content;
} }
public Role getRole() {
return role;
}
} }

View File

@@ -0,0 +1,17 @@
package de.derpandaa.plinfa.dto;
import tools.jackson.databind.JsonNode;
public class OpenAiResponseDto extends AiResponseDto {
public OpenAiResponseDto(String jsonBody) {
super(jsonBody);
}
@Override
protected void parseBody(String jsonBody) {
JsonNode root = mapper.readTree(jsonBody);
response = root.path("choices").path(0).path("message").path("content").asString();
tokensUsed = root.path("usage").path("total_tokens").asInt();
}
}

View File

@@ -0,0 +1,46 @@
package de.derpandaa.plinfa.projects;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import de.derpandaa.plinfa.dto.ChatDto;
public class GenericProject {
private String name;
private List<GenericProject> subProjects;
private List<ChatDto> chats;
public GenericProject(File dir) {
this.name = dir.getName();
this.subProjects = new ArrayList<>();
this.chats = new ArrayList<>();
load(dir);
}
private void load(File dir) {
File[] files = dir.listFiles();
if (files == null)
return;
for (File f : files) {
if (f.isDirectory()) {
subProjects.add(new GenericProject(f));
} else if (f.getName().endsWith(".json")) {
chats.add(new ChatDto(f));
}
}
}
public String getName() {
return name;
}
public List<GenericProject> getSubProjects() {
return subProjects;
}
public List<ChatDto> getChats() {
return chats;
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="#6A5B75" stroke-linecap="round" stroke-width="1.5" d="M15 12h-3m0 0H9m3 0V9m0 3v3m10-3c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12s0-7.071 1.464-8.536C4.93 2 7.286 2 12 2s7.071 0 8.535 1.464c.974.974 1.3 2.343 1.41 4.536" />
</svg>

After

Width:  |  Height:  |  Size: 396 B