summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenjamin Culkin <scorpress@gmail.com>2025-12-07 18:10:18 -0500
committerBenjamin Culkin <scorpress@gmail.com>2025-12-07 18:10:18 -0500
commit3db23673591cb35a9626475a79cdd3474e6a7f06 (patch)
tree8f79c29c92be32d538ad38863f5151a7f6fe697c
parentd9ebbc31e252e275ba1cc28749b106a040726b82 (diff)
Checkpoint commit before fully moving DB connection to app-level
-rw-r--r--firmal/pom.xml17
-rw-r--r--firmal/src/main/java/bjc/firmal/Firmal.java211
-rw-r--r--firmal/src/main/java/bjc/firmal/GalleriaBrowserPanel.java (renamed from firmal/src/main/java/bjc/firmal/FirmalBrowserPanel.java)9
-rw-r--r--firmal/src/main/java/bjc/firmal/GalleriaFrame.java (renamed from firmal/src/main/java/bjc/firmal/FirmalFrame.java)16
-rw-r--r--firmal/src/main/java/bjc/firmal/gptbrowser/GPTConversationDB.java55
-rw-r--r--firmal/src/main/java/bjc/firmal/gptbrowser/GPTJSONBrowserFrame.java522
-rw-r--r--firmal/src/main/java/bjc/firmal/gptbrowser/GPTMessage.java11
-rw-r--r--firmal/src/main/java/bjc/firmal/gptbrowser/RawMessageDB.java43
-rw-r--r--firmal/src/main/java/bjc/firmal/gptbrowser/package-info.java1
-rw-r--r--firmal/src/main/java/module-info.java2
10 files changed, 871 insertions, 16 deletions
diff --git a/firmal/pom.xml b/firmal/pom.xml
index aa1e601..dd41fe1 100644
--- a/firmal/pom.xml
+++ b/firmal/pom.xml
@@ -13,6 +13,23 @@
<artifactId>BJC-Utils2</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.json</groupId>
+ <artifactId>json</artifactId>
+ <version>20160212</version>
+ </dependency>
+ <dependency>
+ <groupId>com.ashardalon</groupId>
+ <artifactId>esodata</artifactId>
+ <version>2.0-SNAPSHOT</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <version>42.7.8</version>
+ </dependency>
+
</dependencies>
<build>
<plugins>
diff --git a/firmal/src/main/java/bjc/firmal/Firmal.java b/firmal/src/main/java/bjc/firmal/Firmal.java
index 715a16a..6e99889 100644
--- a/firmal/src/main/java/bjc/firmal/Firmal.java
+++ b/firmal/src/main/java/bjc/firmal/Firmal.java
@@ -1,28 +1,231 @@
package bjc.firmal;
+import java.awt.BorderLayout;
+import java.awt.event.WindowEvent;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
import javax.swing.JFrame;
+import javax.swing.JInternalFrame;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
+import bjc.firmal.gptbrowser.GPTJSONBrowserFrame;
+import bjc.utils.gui.layout.VLayout;
+import bjc.utils.gui.panels.SimpleInputPanel;
+
/**
* Main class for Firmal.
*
- * Firmal is a image/file browser.
+ * Firmal is a generalized GUI app, currently containing
+ * <ul>
+ * <li>A image/file browser</li>
+ * <li>A utility for reviewing AI conversations</li>
+ * </ul>
*
* @author Ben Culkin
*
*/
public class Firmal {
+ // DB Connection details
+ private String connectURL;
+ private String connectUser;
+ private String connectPassword;
+
+ private Connection dbConnection;
+
+ /**
+ * The public instance of the application
+ */
+ public static final Firmal fm = new Firmal();
+
/**
* General main method.
* @param args Currently unused CLI args.
*/
public static void main(String[] args) {
- JFrame frame = FirmalFrame.createFirmalPane();
- frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ SwingUtilities.invokeLater(() -> fm.buildGUI());
+ }
+
+ private void buildGUI() {
+ try {
+ UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (UnsupportedLookAndFeelException e) {
+ e.printStackTrace();
+ }
+
+ JFrame mainFrame = new JFrame();
+
+ mainFrame.setTitle("Firmal");
+ mainFrame.addWindowStateListener((wev) -> {
+ if (wev.getID() == WindowEvent.WINDOW_CLOSING) {
+ if (dbConnection != null)
+ try {
+ dbConnection.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ mainFrame.dispose();
+ // TODO verify if we need to call System.exit() here, or if disposing our only window is enough
+ }
+ });
+
+ JMenuBar mainMenuBar = new JMenuBar();
+ JMenu galleriaMenu = new JMenu("Galleria");
+ JMenuItem newGalleriaWindow = new JMenuItem("New...");
+ newGalleriaWindow.addActionListener((aev) -> {
+ makeGalleriaPane(mainFrame);
+ });
+ galleriaMenu.add(newGalleriaWindow);
+
+ JMenu chatGPTMenu = new JMenu("ChatGPT Messages");
+ JMenuItem newChatGPTMessageWindow = new JMenuItem("New...");
+ newChatGPTMessageWindow.addActionListener((aev) -> {
+ makeChatGPTPane(mainFrame);
+ });
+ chatGPTMenu.add(newChatGPTMessageWindow);
+
+ JMenu settingsMenu = new JMenu("Settings");
+ JMenuItem dbConnection = new JMenuItem("DB Connection Details...");
+ // NOTE: should this be stored at a application level instead?
+ dbConnection.addActionListener((aev) -> {
+ JDialog dbConnectionDetails = prepareDBSettingsDialog(mainFrame);
+
+ dbConnectionDetails.pack();
+ dbConnectionDetails.setVisible(true);
+ });
+ settingsMenu.add(dbConnection);
+
+ mainMenuBar.add(galleriaMenu);
+ mainMenuBar.add(chatGPTMenu);
+ mainMenuBar.add(settingsMenu);
+ mainFrame.setJMenuBar(mainMenuBar);
+
+ makeChatGPTPane(mainFrame);
+
+ mainFrame.pack();
+ mainFrame.setSize(640, 480);
+
+ mainFrame.setVisible(true);
+ }
+
+ private static void makeGalleriaPane(JFrame mainFrame) {
+ JInternalFrame frame = GalleriaFrame.createGalleriaPane();
+ frame.setSize(840, 680);
frame.pack();
- frame.setSize(640, 480);
+ frame.setVisible(true);
+
+ frame.setClosable(true);
+ frame.setResizable(true);
+ frame.setMaximizable(true);
+ frame.setIconifiable(true);
+ mainFrame.add(frame);
+ }
+
+ private static void makeChatGPTPane(JFrame mainFrame) {
+ JInternalFrame frame = GPTJSONBrowserFrame.makeGPTJSONBrowserFrame(mainFrame);
+ frame.setSize(840, 680);
+ frame.pack();
frame.setVisible(true);
+
+ frame.setClosable(true);
+ frame.setResizable(true);
+ frame.setMaximizable(true);
+ frame.setIconifiable(true);
+
+ mainFrame.add(frame);
+ }
+
+ private JDialog prepareDBSettingsDialog(JFrame parentFrame) {
+ JDialog dbConnectionDetails = new JDialog(parentFrame, "DB Connection Details");
+ dbConnectionDetails.setLayout(new BorderLayout());
+
+ JPanel fieldPanel = new JPanel();
+
+ SimpleInputPanel dbURLPanel = new SimpleInputPanel("DB Connection URL: ", 0);
+ if (connectURL != null) dbURLPanel.inputValue.setText(connectURL);
+ SimpleInputPanel dbUsernamePanel = new SimpleInputPanel("DB Username: ", 80);
+ if (connectUser != null) dbUsernamePanel.inputValue.setText(connectUser);
+
+ JPasswordField dbPasswordField = new JPasswordField(80);
+ if (connectPassword != null) dbPasswordField.setText(connectPassword);
+ JLabel dbPasswordLabel = new JLabel("DB Password: ");
+ JPanel dbPasswordPanel = new JPanel();
+ dbPasswordPanel.add(BorderLayout.LINE_START, dbPasswordLabel);
+ dbPasswordPanel.add(BorderLayout.CENTER, dbPasswordField);
+
+ fieldPanel.setLayout(new VLayout(3));
+
+ fieldPanel.add(dbURLPanel);
+ fieldPanel.add(dbUsernamePanel);
+ fieldPanel.add(dbPasswordPanel);
+
+ JPanel buttonPanel = new JPanel();
+
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener((aev) -> {
+ dbConnectionDetails.dispose();
+ });
+
+ JButton testConnectionButton = new JButton("Test DB Connection");
+ testConnectionButton.addActionListener((aev) -> {
+ try (Connection testConnection = DriverManager.getConnection(dbURLPanel.inputValue.getText(), dbUsernamePanel.inputValue.getText(), new String(dbPasswordField.getPassword()))) {
+ JOptionPane.showMessageDialog(dbConnectionDetails, "Connection successfully established");
+ } catch (SQLException sqlex) {
+ JDialog errorDialog = new JDialog(dbConnectionDetails, "Error connecting to DB");
+
+ JLabel headerLabel = new JLabel("Error connecting to database");
+
+ JLabel errorDetails = new JLabel(sqlex.getLocalizedMessage());
+
+ JButton okButton = new JButton("OK");
+ okButton.addActionListener((aev2) -> {
+ errorDialog.dispose();
+ });
+
+ errorDialog.add(BorderLayout.PAGE_START, headerLabel);
+ errorDialog.add(BorderLayout.PAGE_END, okButton);
+ errorDialog.add(BorderLayout.CENTER, errorDetails);
+
+ errorDialog.pack();
+ errorDialog.setVisible(true);
+ }
+ });
+ JButton submitButton = new JButton("Submit");
+ submitButton.addActionListener((aev) -> {
+ connectURL = dbURLPanel.inputValue.getText();
+ connectUser = dbUsernamePanel.inputValue.getText();
+ connectPassword = new String(dbPasswordField.getPassword());
+
+ dbConnectionDetails.dispose();
+ });
+
+ buttonPanel.add(BorderLayout.LINE_START, cancelButton);
+ buttonPanel.add(BorderLayout.CENTER, testConnectionButton);
+ buttonPanel.add(BorderLayout.LINE_END, submitButton);
+
+ dbConnectionDetails.add(BorderLayout.CENTER, fieldPanel);
+ dbConnectionDetails.add(BorderLayout.PAGE_END, buttonPanel);
+ return dbConnectionDetails;
}
}
diff --git a/firmal/src/main/java/bjc/firmal/FirmalBrowserPanel.java b/firmal/src/main/java/bjc/firmal/GalleriaBrowserPanel.java
index 3120a2d..759db10 100644
--- a/firmal/src/main/java/bjc/firmal/FirmalBrowserPanel.java
+++ b/firmal/src/main/java/bjc/firmal/GalleriaBrowserPanel.java
@@ -9,6 +9,7 @@ import java.io.IOException;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
+import javax.swing.JInternalFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
@@ -19,11 +20,11 @@ import bjc.utils.gui.SimpleKeyedButton;
import bjc.utils.gui.layout.VLayout;
/**
- * Main browser for Firmal.
+ * Main browser for Galleria.
* @author Ben Culkin
*
*/
-public class FirmalBrowserPanel extends JPanel {
+public class GalleriaBrowserPanel extends JPanel {
private static final long serialVersionUID = 9078988253392361649L;
private JEditorPane contentPane;
@@ -33,14 +34,14 @@ public class FirmalBrowserPanel extends JPanel {
private Tape<File> loadedFiles;
- private JFrame root;
+ private JInternalFrame root;
/**
* Create a new browser.
*
* @param root The root window.
*/
- public FirmalBrowserPanel(JFrame root) {
+ public GalleriaBrowserPanel(JInternalFrame root) {
super();
this.loadedFiles = new SingleTape<>();
diff --git a/firmal/src/main/java/bjc/firmal/FirmalFrame.java b/firmal/src/main/java/bjc/firmal/GalleriaFrame.java
index fa093ff..c0d4aad 100644
--- a/firmal/src/main/java/bjc/firmal/FirmalFrame.java
+++ b/firmal/src/main/java/bjc/firmal/GalleriaFrame.java
@@ -5,7 +5,7 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
-import javax.swing.JFrame;
+import javax.swing.JInternalFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
@@ -15,20 +15,20 @@ import bjc.utils.gui.SimpleFileChooser;
import bjc.utils.gui.layout.AutosizeLayout;
/**
- * Pane for Firmal.
+ * Pane for Galleria.
* @author Ben Culkin
*
*/
-public class FirmalFrame {
+public class GalleriaFrame {
/**
- * Create a new Firmal Pane.
- * @return The firmal pane.
+ * Create a new Galleria Pane.
+ * @return The Galleria pane.
*/
- public static JFrame createFirmalPane() {
- JFrame mainframe = new JFrame("Firmal Browser");
+ public static JInternalFrame createGalleriaPane() {
+ JInternalFrame mainframe = new JInternalFrame("Galleria Browser");
mainframe.setLayout(new AutosizeLayout());
- FirmalBrowserPanel browser = new FirmalBrowserPanel(mainframe);
+ GalleriaBrowserPanel browser = new GalleriaBrowserPanel(mainframe);
JMenuBar menuBar = new JMenuBar();
diff --git a/firmal/src/main/java/bjc/firmal/gptbrowser/GPTConversationDB.java b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTConversationDB.java
new file mode 100644
index 0000000..14f9055
--- /dev/null
+++ b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTConversationDB.java
@@ -0,0 +1,55 @@
+package bjc.firmal.gptbrowser;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import bjc.data.Pair;
+
+public class GPTConversationDB {
+ private String id;
+ private String title;
+
+ private List<RawMessageDB> messages;
+
+ private Set<String> messagesSeen;
+
+ public String getID() {
+ return id;
+ }
+ public void setID(String id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void addMessage(RawMessageDB message) {
+ messages.add(message);
+ messagesSeen.add(message.getMessageID());
+
+ // TODO check if there should be a warning for adding a message
+ // that has a different conversation ID than this conversation.
+ }
+ public List<RawMessageDB> getMessages() {
+ return messages;
+ }
+
+ public boolean hasSeenMessage(String msgID) {
+ return messagesSeen.contains(msgID);
+ }
+
+ public GPTConversationDB(String id, String title) {
+ this.id = id;
+ this.title = title;
+
+ this.messages = new ArrayList<>();
+ this.messagesSeen = new HashSet<>();
+ }
+}
diff --git a/firmal/src/main/java/bjc/firmal/gptbrowser/GPTJSONBrowserFrame.java b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTJSONBrowserFrame.java
new file mode 100644
index 0000000..0a87390
--- /dev/null
+++ b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTJSONBrowserFrame.java
@@ -0,0 +1,522 @@
+package bjc.firmal.gptbrowser;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dialog.ModalityType;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JInternalFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollBar;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.JTextPane;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingWorker;
+
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import bjc.utils.gui.DelegateListCellRenderer;
+import bjc.utils.gui.SimpleJList;
+import bjc.utils.gui.layout.HLayout;
+import bjc.utils.gui.layout.VLayout;
+import bjc.utils.gui.panels.SimpleInputPanel;
+import bjc.utils.ioutils.TextAreaOutputStream;
+import bjc.utils.misc.NamedPreparedStatement;
+
+import org.json.JSONArray;
+
+public class GPTJSONBrowserFrame {
+ // Conversation data
+ private JList<GPTConversationDB> conversationListUI;
+ private DefaultListModel<GPTConversationDB> conversationListModel;
+
+ // DB Connection details
+ private String connectURL;
+ private String connectUser;
+ private String connectPassword;
+
+ private final class SaveConversationTask extends SwingWorker<List<Integer>, Integer> {
+
+ @Override
+ protected List<Integer> doInBackground() throws Exception {
+ return null;
+ }
+
+ }
+
+ private final class SaveConversationToDBListener implements ActionListener {
+ private final JFrame parentFrame;
+
+ private SaveConversationToDBListener(JFrame parentFrame) {
+ this.parentFrame = parentFrame;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent aev) {
+ try (Connection testConnection = DriverManager.getConnection(connectURL, connectUser, connectPassword)) {
+ NamedPreparedStatement insertConversation = NamedPreparedStatement.prepare(testConnection,
+ "insert into chatgpt.conversations (conversation_id, conversation_title) values (:id::uuid, :title)"
+ + " on conflict (conversation_id) do nothing");
+ NamedPreparedStatement insertMessage = NamedPreparedStatement.prepare(testConnection,
+ // TODO fix not passing parent_message b/c of constraint reasons
+ "insert into chatgpt.raw_messages (message_id, conversation_id, message_body) "
+ + "values (:selfid::uuid, :convid::uuid, :body::json)"
+ + " on conflict (message_id) do nothing");
+
+ Iterator<GPTConversationDB> conversations = conversationListModel.elements().asIterator();
+ int totalNumConversations = conversationListModel.getSize();
+ int currNumConversations = 0;
+
+ JDialog progressDialog = new JDialog(parentFrame, "DB Save Progress");
+
+ JLabel conversationProgressLabel = new JLabel("Saving Conversation ");
+ JProgressBar conversationProgressBar = new JProgressBar();
+ conversationProgressBar.setMaximum(totalNumConversations);
+ conversationProgressBar.setValue(currNumConversations);
+
+ JPanel conversationProgressPanel = new JPanel();
+ conversationProgressPanel.add(BorderLayout.CENTER, conversationProgressLabel);
+ conversationProgressPanel.add(BorderLayout.PAGE_END, conversationProgressBar);
+
+ JLabel messageProgressLabel = new JLabel("Saving message: ");
+ JProgressBar messageProgressBar = new JProgressBar();
+
+ JPanel messageProgressPanel = new JPanel();
+ messageProgressPanel.add(BorderLayout.CENTER, messageProgressLabel);
+ messageProgressPanel.add(BorderLayout.PAGE_END, messageProgressBar);
+
+ JPanel contentPanel = new JPanel();
+ contentPanel.setLayout(new VLayout(2));
+ contentPanel.add(conversationProgressPanel);
+ contentPanel.add(messageProgressPanel);
+
+ JButton okButton = new JButton("OK");
+ okButton.setEnabled(false);
+ okButton.addActionListener((aev2) -> {
+ progressDialog.dispose();
+ });
+
+ progressDialog.add(BorderLayout.CENTER, contentPanel);
+ progressDialog.add(BorderLayout.PAGE_END, okButton);
+
+ progressDialog.pack();
+ progressDialog.setModalityType(ModalityType.MODELESS);
+// progressDialog.setVisible(true);
+
+ // NOTE: Consider if this should be a text-area thing instead of progress-bar based
+
+ // TODO: Figure out why the dialog isn't updating correctly. I probably
+ // need to use the Swing ExecutorService and split it into tasks to get things to show up properly,
+ // even if that might cause it to be a bit slower
+
+ // DEBUG - Check if this is actually needed
+ int overallMessageCount = 0;
+ final int BATCH_THRESHOLD = 500;
+
+ while (conversations.hasNext()) {
+ currNumConversations++;
+
+ GPTConversationDB conversation = conversations.next();
+
+ conversationProgressBar.setValue(currNumConversations);
+ conversationProgressLabel.setText("Saving Conversation "
+ + currNumConversations + ": " + conversation.getTitle());
+ // DEBUG
+ System.out.println("Saving Conversation "
+ + currNumConversations + ": " + conversation.getTitle());
+
+ insertConversation.setString("id", conversation.getID());
+ insertConversation.setString("title", conversation.getTitle());
+ insertConversation.addBatch();
+
+ List<RawMessageDB> messages = conversation.getMessages();
+ int totalNumMessages = messages.size();
+ int currNumMessages = 0;
+
+ messageProgressBar.setMaximum(totalNumMessages);
+ for (RawMessageDB message : messages) {
+ String parentID = message.getParentMessageID();
+ if (conversation.hasSeenMessage(parentID)) {
+ messageProgressBar.setValue(++currNumMessages);
+ overallMessageCount++;
+ messageProgressLabel.setText("Saving message " + currNumMessages + " of " + totalNumMessages);
+ // DEBUG
+ System.out.println("Saving message " + currNumMessages + " of " + totalNumMessages);
+
+ insertMessage.setString("selfid", message.getMessageID());
+ insertMessage.setString("convid", message.getConversationID());
+ insertMessage.setString("body", message.getMessageBody());
+ // TODO figure out why we are getting constraint violations here.
+ // Do we really need to leave this null initially, then backfill it?
+ // Or do we need to be doing these as independent DB queries instead of batching them?
+ // That sounds rather inefficient, but so is doing a second pass to fill it later
+ // insertMessage.setString("parentid", message.getParentMessageID());
+ insertMessage.addBatch();
+
+ if (overallMessageCount == BATCH_THRESHOLD) {
+ overallMessageCount = 0;
+
+ // DEBUG
+ System.out.println("Starting save of " + BATCH_THRESHOLD + " messages/conversations to the DB");
+
+ // Commit what we have so far to prevent overloading.
+ int[] insertConversationResults = insertConversation.executeBatch();
+ int[] insertMessageResults = insertMessage.executeBatch();
+
+ for (int i : insertConversationResults) {
+ if (i != 0 && i != 1) {
+ // TODO: do something about an oddity
+ }
+ }
+
+ for (int i : insertMessageResults) {
+ if (i != 0 && i != 1) {
+ // TODO: do something about an oddity
+ }
+ }
+
+ // DEBUG
+ System.out.println("Saved " + BATCH_THRESHOLD + " messages/conversations to the DB");
+ }
+ } else {
+ // TODO: this message has a missing/incorrect parent link
+ }
+ }
+ }
+
+ // Prepare to save things
+ int[] insertConversationResults = insertConversation.executeBatch();
+ int[] insertMessageResults = insertMessage.executeBatch();
+
+ for (int i : insertConversationResults) {
+ if (i != 0 && i != 1) {
+ // TODO: do something about an oddity
+ }
+ }
+
+ for (int i : insertMessageResults) {
+ if (i != 0 && i != 1) {
+ // TODO: do something about an oddity
+ }
+ }
+
+ okButton.setEnabled(true);
+
+ insertMessage.close();
+ insertConversation.close();
+ } catch (SQLException sqlex) {
+ JDialog errorDialog = new JDialog(parentFrame, "Error interfacing with DB");
+
+ JLabel headerLabel = new JLabel("Error interfacing with database");
+
+ JLabel errorDetails = new JLabel(sqlex.getLocalizedMessage());
+
+ JButton okButton = new JButton("OK");
+ okButton.addActionListener((aev2) -> {
+ errorDialog.dispose();
+ });
+
+ errorDialog.add(BorderLayout.PAGE_START, headerLabel);
+ errorDialog.add(BorderLayout.PAGE_END, okButton);
+ errorDialog.add(BorderLayout.CENTER, errorDetails);
+
+ errorDialog.pack();
+ errorDialog.setVisible(true);
+ }
+ }
+ }
+
+ private static final class LoadGPTJSONListener implements ActionListener {
+ public static enum ParseMode {
+ /** Capture the whole JSON - don't parse it further. */
+ RAW,
+ /** Parse all of the JSON as completely as possible and store it that way. */
+ COMPLETE,
+ /**
+ * Parse and store only enough of the JSON to provide the user-visible output.
+ *
+ * This skips the thought-records, turn summaries and other associated metadata
+ */
+ EXPORT
+ }
+
+ private final JInternalFrame newFrame;
+ private DefaultListModel<GPTConversationDB> listModel;
+
+ private LoadGPTJSONListener(JInternalFrame newFrame, DefaultListModel<GPTConversationDB> listModel) {
+ this.newFrame = newFrame;
+ this.listModel = listModel;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent aev) {
+ JFileChooser chooser = new JFileChooser();
+ int openResults = chooser.showOpenDialog(newFrame);
+ if (openResults != JFileChooser.APPROVE_OPTION)
+ return;
+
+ List<GPTConversationDB> conversationList = new ArrayList<>();
+ try (FileReader fr = new FileReader(chooser.getSelectedFile())) {
+ JSONTokener loader = new JSONTokener(fr);
+
+ JSONArray conversations = new JSONArray(loader);
+
+ int numConversations = conversations.length();
+ for (int i = 0; i < numConversations; i++) {
+ JSONObject conversation = conversations.optJSONObject(i);
+ if (conversation == null) {
+ // Blank conversation?
+ continue;
+ }
+
+ GPTConversationDB parsedConversation = parseConversationRaw(conversation);
+ conversationList.add(parsedConversation);
+ listModel.addElement(parsedConversation);
+ }
+ } catch (FileNotFoundException fnfex) {
+ // TODO: better error handling
+ fnfex.printStackTrace();
+ } catch (IOException ioex) {
+ ioex.printStackTrace();
+ }
+
+ // TODO present conversations via UI
+ // TODO provide import from DB via JDBC
+ // TODO finalize the primary parsing mode - complete
+ }
+
+
+ private GPTConversationDB parseConversationRaw(JSONObject conversation) {
+ return parseConversation(conversation, ParseMode.RAW);
+ }
+
+ private GPTConversationDB parseConversation(JSONObject conversation, ParseMode mode) {
+
+ String title = conversation.optString("title", "Untitled");
+ String id = conversation.getString("id");
+
+ GPTConversationDB dbConversation = new GPTConversationDB(id, title);
+
+ JSONObject mappings = conversation.getJSONObject("mapping");
+ Iterator<String> mappingKeys = mappings.keys();
+
+ while (mappingKeys.hasNext()) {
+ String mappingKey = mappingKeys.next();
+ JSONObject mapping = mappings.getJSONObject(mappingKey);
+
+ /* These messages possibly need to be organized into a tree using the 'parent' / 'children' fields.
+ Might actually be easier to just store it in the DB and then reconstruct the tree from that
+ info later instead of trying to construct it fully in memory
+ */
+ // Note on the tree thing: What we actually want to do is condense singular tree levels
+
+ if (mapping.opt("message") == null) {
+ // No message, can ignore for now
+ continue;
+ }
+
+ if (mode == ParseMode.RAW) {
+ String selfID = mappingKey;
+ String parentID = mapping.optString("parent", "");
+ String rawMessage = mapping.toString();
+
+ RawMessageDB dbMessage = new RawMessageDB(selfID, id, rawMessage, parentID);
+ dbConversation.addMessage(dbMessage);
+
+ continue;
+ }
+ // Also to consider, do we want to just store the raw messages into the DB?
+ // that will allow us to go back and re-parse them later
+ String selfID = mappingKey;
+ String parentID = mapping.optString("parent", "");
+ // Consider if we should read the children
+
+ JSONObject message = mapping.getJSONObject("message");
+
+ // A field to possibly read is 'recipient' and/or 'channel' which may do a better job
+ // at ID-ing messages that don't need to be visually shown
+ JSONObject messageMetadata = message.optJSONObject("metadata");
+ if (messageMetadata == null
+ || messageMetadata.optBoolean("is_visually_hidden_from_conversation", false) == false) {
+ // Hidden message, skip
+ // NOTE: there do appear to be certain message that we may want to keep regardless
+ continue;
+ }
+ // Consider if we should grab turn_summary from the metadata
+ // Also, metadata has branching_from_conversation_id, branching_from_conversation_title etc
+
+ // In metadata, we also have the attachments object, which contains info about attachments
+
+ // Metadata also has the 'reasoning_status' / 'message_type' fields, but only some of the time.
+
+ // Another metadata field is 'aggregate_result' which seems tied to code output in various ways
+ JSONObject authorData = message.getJSONObject("author");
+
+ String author = authorData.getString("role");
+ // If the author is 'tool', that may need to get handled specially
+ JSONObject messageContent = message.getJSONObject("content");
+
+ String contentType = messageContent.getString("content_type");
+ switch (contentType) {
+ // Should contentType get enum-ified?
+ case "text": {
+ // Text to integrate
+ // NOTE: thing to consider later is that a decent chunk of these text files are markdown
+ // and will need to be displayed that way
+
+ // Also, text that is sent to python can be code that is executed
+ StringBuilder content = new StringBuilder();
+ JSONArray messageParts = messageContent.getJSONArray("parts");
+ int numParts = messageParts.length();
+ for (int j = 0; j < numParts; j++) {
+ // I think we can just collate directly for this content type, but I'm not
+ // convinced it is the right behavior
+ content.append(messageParts.getString(j));
+ }
+ }
+ case "reasoning_recap":
+ case "thoughts":
+ // Consider tying these to their associated message
+ // GPT metadata, worth recording, but not exposing
+ // Notably, `thoughts` seems like it might vary in form depending
+ // on the model or time of use
+ case "code":
+ // This contains code, and will likely need post-processing
+ // There is the 'language' tag for ID'ing languages
+ case "execution_output":
+ // This is also tied to code
+ default:
+ // Unknown content type
+ }
+
+ // NOTE: Given some of the stuff, we probably need a fuller abstraction for this.
+ // The current one is Conversation -> Pairs of User/Assistant message
+ // However, we probably want to use distinct types for those so that we can properly
+ // associate all of the provided metadata to it
+ }
+
+ return dbConversation;
+ }
+ }
+
+ public GPTJSONBrowserFrame() {
+ conversationListModel = new DefaultListModel<>();
+ conversationListUI = new JList<>(conversationListModel);
+
+ conversationListUI.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ conversationListUI.setCellRenderer(new DelegateListCellRenderer<GPTConversationDB>(GPTConversationDB::getTitle));
+ }
+
+ public static JInternalFrame makeGPTJSONBrowserFrame(JFrame parentFrame) {
+ GPTJSONBrowserFrame frame = new GPTJSONBrowserFrame();
+
+ return frame.makeUIFrame(parentFrame);
+ }
+
+ private JInternalFrame makeUIFrame(JFrame parentFrame) {
+ JInternalFrame newFrame = new JInternalFrame("Conversation Browser");
+
+ JMenuBar browserMenuBar = new JMenuBar();
+
+ JMenu importMenu = new JMenu("Import");
+
+ JMenuItem importConversationsFromFile = new JMenuItem("Import Conversations from File...");
+ importConversationsFromFile.addActionListener(new LoadGPTJSONListener(newFrame, conversationListModel));
+
+ importMenu.add(importConversationsFromFile);
+
+ JMenu fileMenu = new JMenu("File");
+
+ JMenuItem saveConversations = new JMenuItem("Save Conversations...");
+ saveConversations.addActionListener(new SaveConversationToDBListener(parentFrame));
+ fileMenu.add(saveConversations);
+
+ JMenuItem loadConversations = new JMenuItem("Load Conversations...");
+ loadConversations.addActionListener((aev) -> {
+ // TODO load conversations from DB
+ });
+ fileMenu.add(loadConversations);
+
+ browserMenuBar.add(fileMenu);
+ browserMenuBar.add(importMenu);
+
+ newFrame.setJMenuBar(browserMenuBar);
+
+ configureConversationUI(newFrame);
+
+ return newFrame;
+ }
+
+ private void configureConversationUI(JInternalFrame newFrame) {
+ JScrollPane scrollList = new JScrollPane(conversationListUI);
+ newFrame.setLayout(new BorderLayout());
+ newFrame.add(BorderLayout.LINE_START, scrollList);
+
+ DefaultListModel<RawMessageDB> conversationMessageModel = new DefaultListModel<>();
+ JList<RawMessageDB> conversationMessageList = new JList<>(conversationMessageModel);
+ JScrollPane conversationMessageScroll = new JScrollPane(conversationMessageList);
+
+ DelegateListCellRenderer<RawMessageDB> messageCellRenderer = new DelegateListCellRenderer<RawMessageDB>(RawMessageDB::getMessageID);
+ conversationMessageList.setCellRenderer(messageCellRenderer);
+ conversationMessageList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ JTextPane messagePane = new JTextPane();
+ JScrollPane messageScrollPane = new JScrollPane(messagePane);
+
+ JSplitPane detailPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, conversationMessageScroll, messageScrollPane);
+
+ newFrame.add(BorderLayout.CENTER, detailPane);
+
+ conversationListUI.addListSelectionListener(lse -> {
+ if (lse.getValueIsAdjusting()) return;
+
+ int idx = lse.getFirstIndex();
+ GPTConversationDB conversation = conversationListModel.get(idx);
+
+ conversationMessageModel.clear();
+
+ for (RawMessageDB rawMessage : conversation.getMessages()) {
+ conversationMessageModel.addElement(rawMessage);
+ }
+ });
+
+ conversationMessageList.addListSelectionListener((lse) -> {
+ if (lse.getValueIsAdjusting()) return;
+
+ int idx = lse.getFirstIndex();
+ RawMessageDB rawMessage = conversationMessageModel.get(idx);
+
+ messagePane.setText(rawMessage.getMessageBody());
+ });
+ }
+}
diff --git a/firmal/src/main/java/bjc/firmal/gptbrowser/GPTMessage.java b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTMessage.java
new file mode 100644
index 0000000..db35156
--- /dev/null
+++ b/firmal/src/main/java/bjc/firmal/gptbrowser/GPTMessage.java
@@ -0,0 +1,11 @@
+package bjc.firmal.gptbrowser;
+
+public class GPTMessage {
+ public static enum MessageAuthor {
+ USER, ASSISTANT,
+ // These seem to mostly be used for 'hidden' messages?
+ TOOL, SYSTEM
+ };
+
+ private MessageAuthor author;
+}
diff --git a/firmal/src/main/java/bjc/firmal/gptbrowser/RawMessageDB.java b/firmal/src/main/java/bjc/firmal/gptbrowser/RawMessageDB.java
new file mode 100644
index 0000000..dd31204
--- /dev/null
+++ b/firmal/src/main/java/bjc/firmal/gptbrowser/RawMessageDB.java
@@ -0,0 +1,43 @@
+package bjc.firmal.gptbrowser;
+
+public class RawMessageDB {
+ private String messageID;
+ private String conversationID;
+ private String messageBody;
+ private String parentMessageID;
+
+ public String getMessageID() {
+ return messageID;
+ }
+ public void setMessageID(String messageID) {
+ this.messageID = messageID;
+ }
+ public String getConversationID() {
+ return conversationID;
+ }
+ public void setConversationID(String conversationID) {
+ this.conversationID = conversationID;
+ }
+ public String getMessageBody() {
+ return messageBody;
+ }
+ public void setMessageBody(String messageBody) {
+ this.messageBody = messageBody;
+ }
+ public String getParentMessageID() {
+ return parentMessageID;
+ }
+ public void setParentMessageID(String parentMessageID) {
+ this.parentMessageID = parentMessageID;
+ }
+
+ public RawMessageDB(String messageID, String conversationID, String messageBody, String parentMessageID) {
+ super();
+ this.messageID = messageID;
+ this.conversationID = conversationID;
+ this.messageBody = messageBody;
+ this.parentMessageID = parentMessageID;
+ }
+
+
+}
diff --git a/firmal/src/main/java/bjc/firmal/gptbrowser/package-info.java b/firmal/src/main/java/bjc/firmal/gptbrowser/package-info.java
new file mode 100644
index 0000000..c48e5d7
--- /dev/null
+++ b/firmal/src/main/java/bjc/firmal/gptbrowser/package-info.java
@@ -0,0 +1 @@
+package bjc.firmal.gptbrowser; \ No newline at end of file
diff --git a/firmal/src/main/java/module-info.java b/firmal/src/main/java/module-info.java
index 80dd7c4..7c434fa 100644
--- a/firmal/src/main/java/module-info.java
+++ b/firmal/src/main/java/module-info.java
@@ -10,4 +10,6 @@ module firmal {
requires bjc.utils;
requires esodata;
requires transitive java.desktop;
+ requires json;
+ requires java.sql;
} \ No newline at end of file