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)