diff options
| -rw-r--r-- | firmal/pom.xml | 17 | ||||
| -rw-r--r-- | firmal/src/main/java/bjc/firmal/Firmal.java | 211 | ||||
| -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.java | 55 | ||||
| -rw-r--r-- | firmal/src/main/java/bjc/firmal/gptbrowser/GPTJSONBrowserFrame.java | 522 | ||||
| -rw-r--r-- | firmal/src/main/java/bjc/firmal/gptbrowser/GPTMessage.java | 11 | ||||
| -rw-r--r-- | firmal/src/main/java/bjc/firmal/gptbrowser/RawMessageDB.java | 43 | ||||
| -rw-r--r-- | firmal/src/main/java/bjc/firmal/gptbrowser/package-info.java | 1 | ||||
| -rw-r--r-- | firmal/src/main/java/module-info.java | 2 |
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 |
