8 |
8 |
9 import json |
9 import json |
10 import os |
10 import os |
11 |
11 |
12 from PyQt6.QtCore import Qt, QTimer, pyqtSlot |
12 from PyQt6.QtCore import Qt, QTimer, pyqtSlot |
13 from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget |
13 from PyQt6.QtWidgets import ( |
|
14 QDialog, |
|
15 QInputDialog, |
|
16 QLineEdit, |
|
17 QMenu, |
|
18 QVBoxLayout, |
|
19 QWidget, |
|
20 ) |
14 |
21 |
15 from eric7 import Globals |
22 from eric7 import Globals |
16 from eric7.EricGui import EricPixmapCache |
23 from eric7.EricGui import EricPixmapCache |
17 from eric7.EricWidgets import EricMessageBox |
24 from eric7.EricWidgets import EricFileDialog, EricMessageBox |
18 from eric7.EricWidgets.EricApplication import ericApp |
25 from eric7.EricWidgets.EricApplication import ericApp |
|
26 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog |
19 |
27 |
20 from .OllamaChatWidget import OllamaChatWidget |
28 from .OllamaChatWidget import OllamaChatWidget |
21 from .OllamaClient import OllamaClient |
29 from .OllamaClient import OllamaClient |
22 from .OllamaHistoryWidget import OllamaHistoryWidget |
30 from .OllamaHistoryWidget import OllamaHistoryWidget |
23 from .Ui_OllamaWidget import Ui_OllamaWidget |
31 from .Ui_OllamaWidget import Ui_OllamaWidget |
62 EricPixmapCache.getIcon( |
70 EricPixmapCache.getIcon( |
63 os.path.join("OllamaInterface", "icons", "send{0}".format(iconSuffix)) |
71 os.path.join("OllamaInterface", "icons", "send{0}".format(iconSuffix)) |
64 ) |
72 ) |
65 ) |
73 ) |
66 |
74 |
|
75 self.ollamaMenuButton.setAutoRaise(True) |
|
76 self.ollamaMenuButton.setShowMenuInside(True) |
|
77 |
67 self.__chatHistoryLayout = QVBoxLayout() |
78 self.__chatHistoryLayout = QVBoxLayout() |
68 self.historyScrollWidget.setLayout(self.__chatHistoryLayout) |
79 self.historyScrollWidget.setLayout(self.__chatHistoryLayout) |
69 self.__chatHistoryLayout.addStretch(1) |
80 self.__chatHistoryLayout.addStretch(1) |
70 |
81 |
71 self.mainSplitter.setSizes([200, 2000]) |
82 self.mainSplitter.setSizes([200, 2000]) |
159 self.modelComboBox.clear() |
173 self.modelComboBox.clear() |
160 |
174 |
161 self.modelComboBox.addItem("") |
175 self.modelComboBox.addItem("") |
162 self.modelComboBox.addItems(sorted(modelNames)) |
176 self.modelComboBox.addItems(sorted(modelNames)) |
163 |
177 |
|
178 @pyqtSlot(list) |
|
179 def __checkHistoryModels(self, modelNames): |
|
180 """ |
|
181 Private slot to set the chat history entry states according to available |
|
182 models. |
|
183 |
|
184 @param modelNames list of model names |
|
185 @type list[str] |
|
186 """ |
|
187 for index in range(self.__chatHistoryLayout.count() - 1): |
|
188 self.__chatHistoryLayout.itemAt(index).widget().checkModelAvailable( |
|
189 modelNames |
|
190 ) |
|
191 |
164 ############################################################################ |
192 ############################################################################ |
165 ## Methods handling signals from the chat history widgets. |
193 ## Methods handling signals from the chat history widgets. |
166 ############################################################################ |
194 ############################################################################ |
167 |
195 |
168 def __createHistoryWidget(self, title, model, jsonStr=None): |
196 def __createHistoryWidget(self, title, model, jsonStr=None): |
241 hid = widget.getId() |
283 hid = widget.getId() |
242 entries[hid] = widget.saveToJson() |
284 entries[hid] = widget.saveToJson() |
243 |
285 |
244 # step 2: save the collected chat histories |
286 # step 2: save the collected chat histories |
245 filePath = self.__historyFilePath() |
287 filePath = self.__historyFilePath() |
|
288 self.__saveChatHistoryFile(filePath, entries) |
|
289 |
|
290 def __saveChatHistoryFile(self, filePath, entries): |
|
291 """ |
|
292 Private method to save the chat history entries to a file. |
|
293 |
|
294 @param filePath file name to save to |
|
295 @type str |
|
296 @param entries dictionary containing the chat history entries as a |
|
297 JSON serialized string indexed by their ID |
|
298 @type dict[str, str] |
|
299 """ |
246 try: |
300 try: |
247 with open(filePath, "w") as f: |
301 with open(filePath, "w") as f: |
248 json.dump(entries, f) |
302 json.dump(entries, f) |
249 except OSError as err: |
303 except OSError as err: |
250 EricMessageBox.critical( |
304 EricMessageBox.critical( |
260 """ |
314 """ |
261 Private method to load a previously saved history file. |
315 Private method to load a previously saved history file. |
262 """ |
316 """ |
263 # step 1: load the history file, if it exists |
317 # step 1: load the history file, if it exists |
264 filePath = self.__historyFilePath() |
318 filePath = self.__historyFilePath() |
|
319 self.__loadChatHistoriesFile(filePath) |
|
320 |
|
321 def __loadChatHistoriesFile(self, filePath, reportDuplicates=False): |
|
322 """ |
|
323 Private method to load chat history entries from a given file. |
|
324 |
|
325 @param filePath path of the chat history file |
|
326 @type str |
|
327 @param reportDuplicates flag indicating to report skipped chat history entries |
|
328 (defaults to False) |
|
329 @type bool (optional) |
|
330 @return flag indicating success |
|
331 @rtype str |
|
332 """ |
265 if not os.path.exists(filePath): |
333 if not os.path.exists(filePath): |
266 return |
334 return False |
267 |
335 |
268 try: |
336 try: |
269 with open(filePath, "r") as f: |
337 with open(filePath, "r") as f: |
270 entries = json.load(f) |
338 entries = json.load(f) |
271 except OSError as err: |
339 except OSError as err: |
275 self.tr( |
343 self.tr( |
276 "<p>The chat history could not be loaded from <b>{0}</b>.</p>" |
344 "<p>The chat history could not be loaded from <b>{0}</b>.</p>" |
277 "<p>Reason: {1}</p>" |
345 "<p>Reason: {1}</p>" |
278 ).format(filePath, str(err)), |
346 ).format(filePath, str(err)), |
279 ) |
347 ) |
280 return |
348 return False |
281 |
349 |
282 # step 2: create history widgets |
350 # step 2: create history widgets |
|
351 existingIDs = self.__getHistoryIds() |
|
352 skipped = [] |
283 for hid in entries: |
353 for hid in entries: |
284 self.__createHistoryWidget("", "", jsonStr=entries[hid]) |
354 if hid in existingIDs: |
|
355 data = json.loads(entries[hid]) |
|
356 skipped.append(data["title"]) |
|
357 else: |
|
358 self.__createHistoryWidget("", "", jsonStr=entries[hid]) |
|
359 |
|
360 if skipped and reportDuplicates: |
|
361 EricMessageBox.warning( |
|
362 self, |
|
363 self.tr("Load Chat History"), |
|
364 self.tr( |
|
365 "<p>These chats were not loaded because they already existed.</p>" |
|
366 "{0}" |
|
367 ).format("<ul><li>{0}</li></ul>".format("</li><li>".join(skipped))), |
|
368 ) |
|
369 |
|
370 return True |
285 |
371 |
286 def clearHistory(self): |
372 def clearHistory(self): |
287 """ |
373 """ |
288 Public method to clear the history entries and close all chats. |
374 Public method to clear the history entries and close all chats. |
289 """ |
375 """ |
290 while self.__chatHistoryLayout.count() > 1: |
376 while self.__chatHistoryLayout.count() > 1: |
291 # do not delete the spacer at the end of the list |
377 # do not delete the spacer at the end of the list |
292 item = self.__chatHistoryLayout.takeAt(0) |
378 item = self.__chatHistoryLayout.takeAt(0) |
293 if item is not None: |
379 if item is not None: |
|
380 hid = item.widget().getId() |
|
381 self.__removeChatWidget(hid) |
294 item.widget().deleteLater() |
382 item.widget().deleteLater() |
295 |
383 |
296 self.__saveHistory() |
384 self.__saveHistory() |
297 |
385 |
298 @pyqtSlot(str) |
386 @pyqtSlot(str) |
506 """ |
594 """ |
507 Private method to create the super menu and attach it to the super |
595 Private method to create the super menu and attach it to the super |
508 menu button. |
596 menu button. |
509 """ |
597 """ |
510 # TODO: implement the menu and menu methods |
598 # TODO: implement the menu and menu methods |
511 # * Clear Chat History |
|
512 # * Show Model Details |
599 # * Show Model Details |
513 # * Show Model Processes |
600 # * Show Model Processes |
514 # * Pull Model |
601 # * Pull Model |
515 # * Show Model Shop (via a web browser) |
602 # * Show Model Shop (via a web browser) |
516 # * Remove Model |
603 # * Remove Model |
|
604 # * Local Server |
|
605 # * Start |
|
606 # * Stop |
|
607 ################################################################### |
|
608 ## Menu with Chat History related actions |
|
609 ################################################################### |
|
610 |
|
611 self.__chatHistoryMenu = QMenu(self.tr("Chat History")) |
|
612 self.__chatHistoryMenu.addAction(self.tr("Load"), self.__loadHistory) |
|
613 self.__chatHistoryMenu.addSeparator() |
|
614 self.__clearHistoriesAct = self.__chatHistoryMenu.addAction( |
|
615 self.tr("Clear All"), self.__menuClearAllHistories |
|
616 ) |
|
617 self.__chatHistoryMenu.addSeparator() |
|
618 self.__chatHistoryMenu.addAction(self.tr("Import"), self.__menuImportHistories) |
|
619 self.__chatHistoryMenu.addAction(self.tr("Export"), self.__menuExportHistories) |
|
620 |
|
621 ################################################################### |
|
622 ## Main menu |
|
623 ################################################################### |
|
624 |
|
625 self.__ollamaMenu = QMenu() |
|
626 self.__ollamaMenu.addMenu(self.__chatHistoryMenu) |
|
627 self.__ollamaMenu.addSeparator() |
|
628 self.__ollamaMenu.addAction(self.tr("Configure..."), self.__ollamaConfigure) |
|
629 |
|
630 self.__ollamaMenu.aboutToShow.connect(self.__aboutToShowOllamaMenu) |
|
631 |
|
632 self.ollamaMenuButton.setMenu(self.__ollamaMenu) |
|
633 |
|
634 @pyqtSlot() |
|
635 def __aboutToShowOllamaMenu(self): |
|
636 """ |
|
637 Private slot to set the action enabled status. |
|
638 """ |
|
639 self.__clearHistoriesAct.setEnabled(self.__chatHistoryLayout.count() > 1) |
|
640 |
|
641 @pyqtSlot() |
|
642 def __ollamaConfigure(self): |
|
643 """ |
|
644 Private slot to show the ollama configuration page. |
|
645 """ |
|
646 ericApp().getObject("UserInterface").showPreferences("ollamaPage") |
|
647 |
|
648 @pyqtSlot() |
|
649 def __menuClearAllHistories(self): |
|
650 """ |
|
651 Private slot to clear all chat history entries. |
|
652 """ |
|
653 yes = EricMessageBox.yesNo( |
|
654 self, |
|
655 self.tr("Clear All Chat Histories"), |
|
656 self.tr( |
|
657 "<p>Do you really want to delete all chat histories? This is" |
|
658 " <b>irreversible</b>.</p>" |
|
659 ), |
|
660 ) |
|
661 if yes: |
|
662 self.clearHistory() |
|
663 |
|
664 @pyqtSlot() |
|
665 def __menuImportHistories(self): |
|
666 """ |
|
667 Private slot to import chat history entries from a file. |
|
668 """ |
|
669 historyFile = EricFileDialog.getOpenFileName( |
|
670 self, |
|
671 self.tr("Import Chat History"), |
|
672 "", |
|
673 self.tr("Chat History Files (*.json);;All Files (*)"), |
|
674 self.tr("Chat History Files (*.json)"), |
|
675 ) |
|
676 if historyFile: |
|
677 self.__loadChatHistoriesFile(historyFile, reportDuplicates=True) |
|
678 |
|
679 @pyqtSlot() |
|
680 def __menuExportHistories(self): |
|
681 """ |
|
682 Private slot to export chat history entries to a file. |
|
683 """ |
|
684 entries = [] |
|
685 for index in range(self.__chatHistoryLayout.count() - 1): |
|
686 item = self.__chatHistoryLayout.itemAt(index) |
|
687 widget = item.widget() |
|
688 hid = widget.getId() |
|
689 title = widget.getTitle() |
|
690 entries.append((title, hid)) |
|
691 |
|
692 dlg = EricListSelectionDialog( |
|
693 entries, |
|
694 title=self.tr("Export Chat History"), |
|
695 message=self.tr("Select the chats to be exported:"), |
|
696 checkBoxSelection=True, |
|
697 showSelectAll=True, |
|
698 ) |
|
699 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
700 selectedChats = dlg.getSelection() |
|
701 |
|
702 fileName = EricFileDialog.getSaveFileName( |
|
703 self, |
|
704 self.tr("Export Chat History"), |
|
705 "", |
|
706 self.tr("Chat History Files (*.json)"), |
|
707 None, |
|
708 EricFileDialog.DontConfirmOverwrite, |
|
709 ) |
|
710 if fileName: |
|
711 if not fileName.endswith(".json"): |
|
712 fileName += ".json" |
|
713 |
|
714 entries = {} |
|
715 for _, hid in selectedChats: |
|
716 historyWidget = self.__findHistoryWidget(hid) |
|
717 if historyWidget is not None: |
|
718 entries[hid] = historyWidget.saveToJson() |
|
719 self.__saveChatHistoryFile(fileName, entries) |