diff --git a/pom.xml b/pom.xml index c0c8178..ce9896b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,16 @@ - - 4.0.0 - de.derpandaa - plinfa - 0.0.1-SNAPSHOT - - 6.10.0 + + 4.0.0 + de.derpandaa + plinfa + 0.0.1-SNAPSHOT + + 6.11.0 3.0.3 + 25 - + io.qtjambi qtjambi @@ -18,6 +21,16 @@ qtjambi-native-linux-x64 ${qtjambi.version} + + com.formdev + flatlaf + 3.4.1 + + + com.formdev + flatlaf-extras + 3.4.1 + tools.jackson.core jackson-core diff --git a/src/main/java/de/derpandaa/aitestsuit/ChatGPTApiWrapper.java b/src/main/java/de/derpandaa/aitestsuit/ChatGPTApiWrapper.java deleted file mode 100644 index e0fee73..0000000 --- a/src/main/java/de/derpandaa/aitestsuit/ChatGPTApiWrapper.java +++ /dev/null @@ -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 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 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; - }); - } -} diff --git a/src/main/java/de/derpandaa/aitestsuit/ChatWindow.java b/src/main/java/de/derpandaa/aitestsuit/ChatWindow.java deleted file mode 100644 index 2d70f49..0000000 --- a/src/main/java/de/derpandaa/aitestsuit/ChatWindow.java +++ /dev/null @@ -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"); - } -} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/aitestsuit/OpenAiDto.java b/src/main/java/de/derpandaa/aitestsuit/OpenAiDto.java deleted file mode 100644 index 2f4f14a..0000000 --- a/src/main/java/de/derpandaa/aitestsuit/OpenAiDto.java +++ /dev/null @@ -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 messages; - - public OpenAiDto(Model model, MessageDto... messages) { - this.model = model; - this.messages = new ArrayList(); - this.messages.addAll(Arrays.asList(messages)); - } - - public void addMessage(MessageDto messageDto) { - messages.add(messageDto); - } - - public List getMessages() { - return messages; - } - - public void setModel(Model model) { - this.model = model; - } - -} diff --git a/src/main/java/de/derpandaa/aitestsuit/AiWindow.java b/src/main/java/de/derpandaa/plinfa/AiWindow.java similarity index 76% rename from src/main/java/de/derpandaa/aitestsuit/AiWindow.java rename to src/main/java/de/derpandaa/plinfa/AiWindow.java index 20fc50c..807ac0c 100644 --- a/src/main/java/de/derpandaa/aitestsuit/AiWindow.java +++ b/src/main/java/de/derpandaa/plinfa/AiWindow.java @@ -1,4 +1,4 @@ -package de.derpandaa.aitestsuit; +package de.derpandaa.plinfa; import io.qt.widgets.QApplication; import io.qt.widgets.QBoxLayout; @@ -9,12 +9,9 @@ import io.qt.widgets.QTextEdit; import io.qt.widgets.QVBoxLayout; import io.qt.widgets.QWidget; -public class AiWindow { - - - public static void main(String[] args) { - QApplication.initialize(args); - QMainWindow window = new QMainWindow(); +public class AiWindow extends QMainWindow { + public AiWindow() { + super(); QWidget widget = new QWidget(); QBoxLayout layout = new QVBoxLayout(); @@ -39,16 +36,23 @@ public class AiWindow { QPushButton buttonRun = new QPushButton("Run"); buttonRun.clicked.connect(() -> { - String result = ChatGPTApiWrapper.run(editSystemPrompt.getPlainText(), editUserPrompt.getPlainText()); - editAiAnser.setText(result); +// String result = AiProviderManager.getAiProviderManager().run(editSystemPrompt.getPlainText(), +// editUserPrompt.getPlainText()); +// editAiAnser.setText(result); }); layout.addWidget(buttonRun); widget.setLayout(layout); - window.setCentralWidget(widget); - window.show(); + setCentralWidget(widget); + show(); + } + + public static void main(String[] args) { + QApplication.initialize(args); + AiWindow window = new AiWindow(); + QApplication.exec(); QApplication.shutdown(); } diff --git a/src/main/java/de/derpandaa/plinfa/ChatListWidget.java b/src/main/java/de/derpandaa/plinfa/ChatListWidget.java new file mode 100644 index 0000000..dc84ed0 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/ChatListWidget.java @@ -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 clickHandler; + + public ChatListWidget(Consumer 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); + } + +} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/plinfa/ChatWindow.java b/src/main/java/de/derpandaa/plinfa/ChatWindow.java new file mode 100644 index 0000000..2a79cab --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/ChatWindow.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/plinfa/MessageBubble.java b/src/main/java/de/derpandaa/plinfa/MessageBubble.java new file mode 100644 index 0000000..845266c --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/MessageBubble.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/plinfa/PlinfaMenu.java b/src/main/java/de/derpandaa/plinfa/PlinfaMenu.java new file mode 100644 index 0000000..b96ac32 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/PlinfaMenu.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/plinfa/PlinfaWindow.java b/src/main/java/de/derpandaa/plinfa/PlinfaWindow.java new file mode 100644 index 0000000..affa31f --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/PlinfaWindow.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/de/derpandaa/plinfa/aiprovider/AbstractAiProvider.java b/src/main/java/de/derpandaa/plinfa/aiprovider/AbstractAiProvider.java new file mode 100644 index 0000000..028fe00 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/aiprovider/AbstractAiProvider.java @@ -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 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 response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + return parseResponse(response.body()); + } catch (IOException | InterruptedException e) { + // NOOP + } + return null; + + } + + public void runAsync(AiRequest request, Consumer 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; +} diff --git a/src/main/java/de/derpandaa/plinfa/aiprovider/AiProviderManager.java b/src/main/java/de/derpandaa/plinfa/aiprovider/AiProviderManager.java new file mode 100644 index 0000000..0db5c8c --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/aiprovider/AiProviderManager.java @@ -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 providers; + private static AiProviderManager aiProviderManager; + private List models = new ArrayList(); + + public AiProviderManager() { + providers = new ArrayList(); + if (OpenAiProvider.API_KEY != null) { + providers.add(new OpenAiProvider()); + } + } + + public static AiProviderManager getAiProviderManager() { + if (aiProviderManager == null) { + aiProviderManager = new AiProviderManager(); + } + return aiProviderManager; + } + + public List 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 onResult) { + getProviderForModel(aiRequest.getModel()).runAsync(aiRequest, onResult); + } + +} diff --git a/src/main/java/de/derpandaa/plinfa/aiprovider/Model.java b/src/main/java/de/derpandaa/plinfa/aiprovider/Model.java new file mode 100644 index 0000000..804e614 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/aiprovider/Model.java @@ -0,0 +1,16 @@ +package de.derpandaa.plinfa.aiprovider; + +import com.fasterxml.jackson.annotation.JsonValue; + +public record Model(String modelName, Class clazz) { + @Override + public String toString() { + return modelName; + } + + @JsonValue + public String modelName() { + return modelName; + } + +} diff --git a/src/main/java/de/derpandaa/plinfa/aiprovider/OpenAiProvider.java b/src/main/java/de/derpandaa/plinfa/aiprovider/OpenAiProvider.java new file mode 100644 index 0000000..2c07f91 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/aiprovider/OpenAiProvider.java @@ -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 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); + } + +} diff --git a/src/main/java/de/derpandaa/plinfa/dto/AiRequest.java b/src/main/java/de/derpandaa/plinfa/dto/AiRequest.java new file mode 100644 index 0000000..4671d43 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/dto/AiRequest.java @@ -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 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 getMessages() { + return messages; + } + + public void addMessages(MessageDto message) { + messages.add(message); + } +} diff --git a/src/main/java/de/derpandaa/plinfa/dto/AiResponseDto.java b/src/main/java/de/derpandaa/plinfa/dto/AiResponseDto.java new file mode 100644 index 0000000..13b6ad6 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/dto/AiResponseDto.java @@ -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; + } +} diff --git a/src/main/java/de/derpandaa/plinfa/dto/AnthropicResponseDto.java b/src/main/java/de/derpandaa/plinfa/dto/AnthropicResponseDto.java new file mode 100644 index 0000000..07afb6b --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/dto/AnthropicResponseDto.java @@ -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(); + } + +} diff --git a/src/main/java/de/derpandaa/plinfa/dto/ChatDto.java b/src/main/java/de/derpandaa/plinfa/dto/ChatDto.java new file mode 100644 index 0000000..18c8153 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/dto/ChatDto.java @@ -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 messages; + + public ChatDto() { + messages = new ArrayList(); + } + + public ChatDto(File f) { + loadFromFile(f); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List 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>() { + }); + } 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; + } +} diff --git a/src/main/java/de/derpandaa/aitestsuit/MessageDto.java b/src/main/java/de/derpandaa/plinfa/dto/MessageDto.java similarity index 65% rename from src/main/java/de/derpandaa/aitestsuit/MessageDto.java rename to src/main/java/de/derpandaa/plinfa/dto/MessageDto.java index 7ba996a..fab661a 100644 --- a/src/main/java/de/derpandaa/aitestsuit/MessageDto.java +++ b/src/main/java/de/derpandaa/plinfa/dto/MessageDto.java @@ -1,7 +1,9 @@ -package de.derpandaa.aitestsuit; +package de.derpandaa.plinfa.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +@JsonPropertyOrder({ "role", "content" }) public class MessageDto { public enum Role { @@ -13,6 +15,9 @@ public class MessageDto { @JsonProperty("content") private String content; + public MessageDto() { + } + public MessageDto(Role role, String content) { this.role = role; this.content = content; @@ -22,4 +27,8 @@ public class MessageDto { return content; } + public Role getRole() { + return role; + } + } diff --git a/src/main/java/de/derpandaa/plinfa/dto/OpenAiResponseDto.java b/src/main/java/de/derpandaa/plinfa/dto/OpenAiResponseDto.java new file mode 100644 index 0000000..f6a2ad5 --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/dto/OpenAiResponseDto.java @@ -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(); + } + +} diff --git a/src/main/java/de/derpandaa/plinfa/projects/GenericProject.java b/src/main/java/de/derpandaa/plinfa/projects/GenericProject.java new file mode 100644 index 0000000..22b1a8c --- /dev/null +++ b/src/main/java/de/derpandaa/plinfa/projects/GenericProject.java @@ -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 subProjects; + private List 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 getSubProjects() { + return subProjects; + } + + public List getChats() { + return chats; + } +} diff --git a/src/main/resources/img/add-icon.svg b/src/main/resources/img/add-icon.svg new file mode 100644 index 0000000..a6370bc --- /dev/null +++ b/src/main/resources/img/add-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file