OllamaInterface/OllamaWidget.py

changeset 6
d8064fb63eac
parent 5
6e8af43d537d
child 7
eb1dec15b2f0
diff -r 6e8af43d537d -r d8064fb63eac OllamaInterface/OllamaWidget.py
--- a/OllamaInterface/OllamaWidget.py	Tue Aug 06 18:18:39 2024 +0200
+++ b/OllamaInterface/OllamaWidget.py	Wed Aug 07 18:19:25 2024 +0200
@@ -10,12 +10,20 @@
 import os
 
 from PyQt6.QtCore import Qt, QTimer, pyqtSlot
-from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget
+from PyQt6.QtWidgets import (
+    QDialog,
+    QInputDialog,
+    QLineEdit,
+    QMenu,
+    QVBoxLayout,
+    QWidget,
+)
 
 from eric7 import Globals
 from eric7.EricGui import EricPixmapCache
-from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets import EricFileDialog, EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
 
 from .OllamaChatWidget import OllamaChatWidget
 from .OllamaClient import OllamaClient
@@ -64,6 +72,9 @@
             )
         )
 
+        self.ollamaMenuButton.setAutoRaise(True)
+        self.ollamaMenuButton.setShowMenuInside(True)
+
         self.__chatHistoryLayout = QVBoxLayout()
         self.historyScrollWidget.setLayout(self.__chatHistoryLayout)
         self.__chatHistoryLayout.addStretch(1)
@@ -75,6 +86,8 @@
 
         self.__connectClient()
 
+        self.__initOllamaMenu()
+
         self.sendButton.clicked.connect(self.__sendMessage)
         self.messageEdit.returnPressed.connect(self.__sendMessage)
 
@@ -88,6 +101,7 @@
         self.__client.serverStateChanged.connect(self.__handleServerStateChanged)
         self.__client.serverVersion.connect(self.__setHeaderLabel)
         self.__client.modelsList.connect(self.__populateModelSelector)
+        self.__client.modelsList.connect(self.__checkHistoryModels)
         self.__client.replyReceived.connect(self.__handleServerMessage)
 
     @pyqtSlot(bool)
@@ -161,6 +175,20 @@
         self.modelComboBox.addItem("")
         self.modelComboBox.addItems(sorted(modelNames))
 
+    @pyqtSlot(list)
+    def __checkHistoryModels(self, modelNames):
+        """
+        Private slot to set the chat history entry states according to available
+        models.
+
+        @param modelNames list of model names
+        @type list[str]
+        """
+        for index in range(self.__chatHistoryLayout.count() - 1):
+            self.__chatHistoryLayout.itemAt(index).widget().checkModelAvailable(
+                modelNames
+            )
+
     ############################################################################
     ## Methods handling signals from the chat history widgets.
     ############################################################################
@@ -220,6 +248,20 @@
 
         return None
 
+    def __getHistoryIds(self):
+        """
+        Private method to get a list of all history IDs.
+
+        @return list of history IDs
+        @rtype list[str]
+        """
+        hids = []
+        for index in range(self.__chatHistoryLayout.count() - 1):
+            widget = self.__chatHistoryLayout.itemAt(index).widget()
+            hids.append(widget.getId())
+
+        return hids
+
     def __historyFilePath(self):
         """
         Private method to get the path name of the chat history file.
@@ -243,6 +285,18 @@
 
         # step 2: save the collected chat histories
         filePath = self.__historyFilePath()
+        self.__saveChatHistoryFile(filePath, entries)
+
+    def __saveChatHistoryFile(self, filePath, entries):
+        """
+        Private method to save the chat history entries to a file.
+
+        @param filePath file name to save to
+        @type str
+        @param entries dictionary containing the chat history entries as a
+            JSON serialized string indexed by their ID
+        @type dict[str, str]
+        """
         try:
             with open(filePath, "w") as f:
                 json.dump(entries, f)
@@ -262,8 +316,22 @@
         """
         # step 1: load the history file, if it exists
         filePath = self.__historyFilePath()
+        self.__loadChatHistoriesFile(filePath)
+
+    def __loadChatHistoriesFile(self, filePath, reportDuplicates=False):
+        """
+        Private method to load chat history entries from a given file.
+
+        @param filePath path of the chat history file
+        @type str
+        @param reportDuplicates flag indicating to report skipped chat history entries
+            (defaults to False)
+        @type bool (optional)
+        @return flag indicating success
+        @rtype str
+        """
         if not os.path.exists(filePath):
-            return
+            return False
 
         try:
             with open(filePath, "r") as f:
@@ -277,11 +345,29 @@
                     "<p>Reason: {1}</p>"
                 ).format(filePath, str(err)),
             )
-            return
+            return False
 
         # step 2: create history widgets
+        existingIDs = self.__getHistoryIds()
+        skipped = []
         for hid in entries:
-            self.__createHistoryWidget("", "", jsonStr=entries[hid])
+            if hid in existingIDs:
+                data = json.loads(entries[hid])
+                skipped.append(data["title"])
+            else:
+                self.__createHistoryWidget("", "", jsonStr=entries[hid])
+
+        if skipped and reportDuplicates:
+            EricMessageBox.warning(
+                self,
+                self.tr("Load Chat History"),
+                self.tr(
+                    "<p>These chats were not loaded because they already existed.</p>"
+                    "{0}"
+                ).format("<ul><li>{0}</li></ul>".format("</li><li>".join(skipped))),
+            )
+
+        return True
 
     def clearHistory(self):
         """
@@ -291,6 +377,8 @@
             # do not delete the spacer at the end of the list
             item = self.__chatHistoryLayout.takeAt(0)
             if item is not None:
+                hid = item.widget().getId()
+                self.__removeChatWidget(hid)
                 item.widget().deleteLater()
 
         self.__saveHistory()
@@ -508,9 +596,124 @@
         menu button.
         """
         # TODO: implement the menu and menu methods
-        #       * Clear Chat History
         #       * Show Model Details
         #       * Show Model Processes
         #       * Pull Model
         #       * Show Model Shop (via a web browser)
         #       * Remove Model
+        #       * Local Server
+        #           * Start
+        #           * Stop
+        ###################################################################
+        ## Menu with Chat History related actions
+        ###################################################################
+
+        self.__chatHistoryMenu = QMenu(self.tr("Chat History"))
+        self.__chatHistoryMenu.addAction(self.tr("Load"), self.__loadHistory)
+        self.__chatHistoryMenu.addSeparator()
+        self.__clearHistoriesAct = self.__chatHistoryMenu.addAction(
+            self.tr("Clear All"), self.__menuClearAllHistories
+        )
+        self.__chatHistoryMenu.addSeparator()
+        self.__chatHistoryMenu.addAction(self.tr("Import"), self.__menuImportHistories)
+        self.__chatHistoryMenu.addAction(self.tr("Export"), self.__menuExportHistories)
+
+        ###################################################################
+        ## Main menu
+        ###################################################################
+
+        self.__ollamaMenu = QMenu()
+        self.__ollamaMenu.addMenu(self.__chatHistoryMenu)
+        self.__ollamaMenu.addSeparator()
+        self.__ollamaMenu.addAction(self.tr("Configure..."), self.__ollamaConfigure)
+
+        self.__ollamaMenu.aboutToShow.connect(self.__aboutToShowOllamaMenu)
+
+        self.ollamaMenuButton.setMenu(self.__ollamaMenu)
+
+    @pyqtSlot()
+    def __aboutToShowOllamaMenu(self):
+        """
+        Private slot to set the action enabled status.
+        """
+        self.__clearHistoriesAct.setEnabled(self.__chatHistoryLayout.count() > 1)
+
+    @pyqtSlot()
+    def __ollamaConfigure(self):
+        """
+        Private slot to show the ollama configuration page.
+        """
+        ericApp().getObject("UserInterface").showPreferences("ollamaPage")
+
+    @pyqtSlot()
+    def __menuClearAllHistories(self):
+        """
+        Private slot to clear all chat history entries.
+        """
+        yes = EricMessageBox.yesNo(
+            self,
+            self.tr("Clear All Chat Histories"),
+            self.tr(
+                "<p>Do you really want to delete all chat histories? This is"
+                " <b>irreversible</b>.</p>"
+            ),
+        )
+        if yes:
+            self.clearHistory()
+
+    @pyqtSlot()
+    def __menuImportHistories(self):
+        """
+        Private slot to import chat history entries from a file.
+        """
+        historyFile = EricFileDialog.getOpenFileName(
+            self,
+            self.tr("Import Chat History"),
+            "",
+            self.tr("Chat History Files (*.json);;All Files (*)"),
+            self.tr("Chat History Files (*.json)"),
+        )
+        if historyFile:
+            self.__loadChatHistoriesFile(historyFile, reportDuplicates=True)
+
+    @pyqtSlot()
+    def __menuExportHistories(self):
+        """
+        Private slot to export chat history entries to a file.
+        """
+        entries = []
+        for index in range(self.__chatHistoryLayout.count() - 1):
+            item = self.__chatHistoryLayout.itemAt(index)
+            widget = item.widget()
+            hid = widget.getId()
+            title = widget.getTitle()
+            entries.append((title, hid))
+
+        dlg = EricListSelectionDialog(
+            entries,
+            title=self.tr("Export Chat History"),
+            message=self.tr("Select the chats to be exported:"),
+            checkBoxSelection=True,
+            showSelectAll=True,
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            selectedChats = dlg.getSelection()
+
+            fileName = EricFileDialog.getSaveFileName(
+                self,
+                self.tr("Export Chat History"),
+                "",
+                self.tr("Chat History Files (*.json)"),
+                None,
+                EricFileDialog.DontConfirmOverwrite,
+            )
+            if fileName:
+                if not fileName.endswith(".json"):
+                    fileName += ".json"
+
+                entries = {}
+                for _, hid in selectedChats:
+                    historyWidget = self.__findHistoryWidget(hid)
+                    if historyWidget is not None:
+                        entries[hid] = historyWidget.saveToJson()
+                self.__saveChatHistoryFile(fileName, entries)

eric ide

mercurial