Tue, 06 Aug 2024 18:18:39 +0200
Implemented the 'chat' functionality.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/AutoResizeTextBrowser.py Tue Aug 06 18:18:39 2024 +0200 @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a QTextBrowser widget that resizes automatically. +""" + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QFrame, QSizePolicy, QTextBrowser + + +class AutoResizeTextBrowser(QTextBrowser): + """ + Class implementing a QTextBrowser widget that resizes automatically. + """ + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent=parent) + + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setFrameShape(QFrame.Shape.NoFrame) + + self.textChanged.connect(self.updateGeometry) + + def resizeEvent(self, evt): + """ + Protected method to handle resize events. + + @param evt reference to the resize event + @type QResizeEvent + """ + super().resizeEvent(evt) + self.updateGeometry() + + def updateGeometry(self): + """ + Public method to update the geometry depending on the current text. + """ + # Set the text width of the document to match the width of the text browser. + self.document().setTextWidth( + self.width() - 2 * int(self.document().documentMargin()) + ) + + # Get the document height and set it as the fixed height of the text browser. + docHeight = self.document().size().height() + self.setFixedHeight(int(docHeight)) + + # Call the base class updateGeometry() method. + super().updateGeometry()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/OllamaChatMessageBox.py Tue Aug 06 18:18:39 2024 +0200 @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a message box widget showing the role and content of a message. +""" + +import os + +from PyQt6.QtCore import QSize, Qt +from PyQt6.QtWidgets import QHBoxLayout, QLabel, QWidget + +from eric7.EricGui import EricPixmapCache +from eric7.EricWidgets.EricApplication import ericApp + +from .AutoResizeTextBrowser import AutoResizeTextBrowser + + +class OllamaChatMessageBox(QWidget): + """ + Class implementing a message box widget showing the role and content of a message. + """ + + def __init__(self, role, message, parent=None): + """ + Constructor + + @param role role of the message sender (one of 'user' or 'assistant') + @type str + @param message message text + @type str + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.__roleLabel = QLabel(self) + self.__roleLabel.setFixedSize(22, 22) + pixmapName = "{0}-{1}".format( + "user" if role == "user" else "ollama22", + "dark" if ericApp().usesDarkPalette() else "light", + ) + self.__roleLabel.setPixmap( + EricPixmapCache.getPixmap( + os.path.join("OllamaInterface", "icons", pixmapName), + QSize(22, 22), + ) + ) + self.__roleLabel.setAlignment( + Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop + ) + + self.__messageBrowser = AutoResizeTextBrowser(self) + + self.__layout = QHBoxLayout(self) + self.__layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.__layout.setContentsMargins(0, 0, 0, 0) + self.__layout.addWidget(self.__roleLabel, Qt.AlignmentFlag.AlignTop) + self.__layout.addWidget(self.__messageBrowser) + self.setLayout(self.__layout) + + self.__message = "" + self.appendMessage(message) + + def appendMessage(self, msg): + """ + Public method to append the given message text to the current content. + + @param msg message to be appended + @type str + """ + if msg: + self.__message += msg + self.__messageBrowser.setMarkdown(self.__message) + + def getMessage(self): + """ + Public method to get the message content. + + @return message content + @rtype str + """ + return self.__message
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/OllamaChatWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget showing the chat with the 'ollama' server. +""" + +from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtWidgets import QVBoxLayout, QWidget + +from .OllamaChatMessageBox import OllamaChatMessageBox +from .Ui_OllamaChatWidget import Ui_OllamaChatWidget + + +class OllamaChatWidget(QWidget, Ui_OllamaChatWidget): + """ + Class implementing a widget showing the chat with the 'ollama' server. + """ + + def __init__(self, hid, title, model, parent=None): + """ + Constructor + + @param hid ID of the chat history + @type str + @param title title of the chat + @type str + @param model model name used for the chat + @type str + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.__hid = hid + + self.headerLabel.setText( + self.tr("<b>{0} - {1}</b>", "title, model name").format(title, model) + ) + + self.__messagesLayout = QVBoxLayout() + self.__messagesLayout.setContentsMargins(4, 4, 4, 4) + self.__messagesLayout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.chatMessagesWidget.setLayout(self.__messagesLayout) + + def addMessage(self, role, message): + """ + Public method to add a new message. + + @param role role of the message sender (one of 'user' or 'assistant') + @type str + @param message message text + @type str + """ + msgWidget = OllamaChatMessageBox(role=role, message=message) + self.__messagesLayout.addWidget(msgWidget) + + QTimer.singleShot(0, self.__scrollChatToBottom) + + def appendMessage(self, message): + """ + Public method to append a given message to the bottom most message box. + + @param message message text to be appended + @type str + """ + msgBox = self.__messagesLayout.itemAt( + self.__messagesLayout.count() - 1 + ).widget() + msgBox.appendMessage(message) + + QTimer.singleShot(0, self.__scrollChatToBottom) + + @pyqtSlot() + def __scrollChatToBottom(self): + """ + Private slot to scroll the chat scroll area to the bottom. + """ + scrollbar = self.chatMessagesScrollArea.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def getHistoryId(self): + """ + Public method to get the history ID of this chat. + + @return DESCRIPTION + @rtype TYPE + """ + return self.__hid + + def getRecentMessage(self): + """ + Public method to get the message of the last message box. + + @return message content + @rtype str + """ + msgBox = self.__messagesLayout.itemAt( + self.__messagesLayout.count() - 1 + ).widget() + return msgBox.getMessage()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/OllamaChatWidget.ui Tue Aug 06 18:18:39 2024 +0200 @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OllamaChatWidget</class> + <widget class="QWidget" name="OllamaChatWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>598</width> + <height>514</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string notr="true">Chat Header</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QScrollArea" name="chatMessagesScrollArea"> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="chatMessagesWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>578</width> + <height>466</height> + </rect> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- a/OllamaInterface/OllamaClient.py Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/OllamaClient.py Tue Aug 06 18:18:39 2024 +0200 @@ -41,8 +41,8 @@ """ Class implementing the 'ollama' client. - @signal replyReceived(content:str, role:str) emitted after a response from the - 'ollama' server was received + @signal replyReceived(content:str, role:str, done:bool) emitted after a response + from the 'ollama' server was received @signal modelsList(modelNames:list[str]) emitted after the list of model names was obtained from the 'ollama' server @signal detailedModelsList(models:list[dict]) emitted after the list of @@ -61,7 +61,7 @@ responsiveness """ - replyReceived = pyqtSignal(str, str) + replyReceived = pyqtSignal(str, str, bool) modelsList = pyqtSignal(list) detailedModelsList = pyqtSignal(list) runningModelsList = pyqtSignal(list) @@ -101,7 +101,7 @@ self.__plugin.preferencesChanged.connect(self.__setHeartbeatTimer) self.__setHeartbeatTimer() - def chat(self, model, messages): + def chat(self, model, messages, streaming=True): """ Public method to request a chat completion from the 'ollama' server. @@ -109,11 +109,13 @@ @type str @param messages list of message objects @type list of dict + @param streaming flag indicating to receive a streaming response + @type bool """ - # TODO: not implemented yet ollamaRequest = { "model": model, "messages": messages, + "stream": streaming, } self.__sendRequest( "chat", data=ollamaRequest, processResponse=self.__processChatResponse @@ -128,8 +130,9 @@ """ with contextlib.suppress(KeyError): message = response["message"] + done = response["done"] if message: - self.replyReceived.emit(message["content"], message["role"]) + self.replyReceived.emit(message["content"], message["role"], done) def generate(self, model, prompt, suffix=None): """ @@ -142,7 +145,6 @@ @param suffix text after the model response (defaults to None) @type str (optional) """ - # TODO: not implemented yet ollamaRequest = { "model": model, "prompt": prompt, @@ -163,7 +165,7 @@ @type dict """ with contextlib.suppress(KeyError): - self.replyReceived.emit(response["response"], "") + self.replyReceived.emit(response["response"], "", response["done"]) def pull(self, model): """
--- a/OllamaInterface/OllamaHistoryWidget.py Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/OllamaHistoryWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -10,7 +10,7 @@ import uuid from PyQt6.QtCore import pyqtSignal, pyqtSlot -from PyQt6.QtWidgets import QWidget +from PyQt6.QtWidgets import QInputDialog, QLineEdit, QWidget from eric7.EricGui import EricPixmapCache @@ -50,6 +50,7 @@ self.setupUi(self) self.newChatButton.setIcon(EricPixmapCache.getIcon("plus")) + self.editButton.setIcon(EricPixmapCache.getIcon("editRename")) self.deleteButton.setIcon(EricPixmapCache.getIcon("trash")) if jsonStr is None: @@ -64,15 +65,42 @@ self.titleEdit.setText(self.__title) self.modelEdit.setText(self.__model) + def getTitle(self): + """ + Public method to get the chat title. + + @return chat title + @rtype str + """ + return self.__title + + def getModel(self): + """ + Public method to get the model used by the chat. + + @return model name + @rtype str + """ + return self.__model + def getId(self): """ Public method to get the chat history ID. - + @return ID of the history entry @rtype str """ return self.__id + def getMessages(self): + """ + Public method to get the list of messages. + + @return list of stored messages + @rtype list[dict[str, str]] + """ + return self.__messages + @pyqtSlot() def on_deleteButton_clicked(self): """ @@ -87,6 +115,23 @@ """ self.newChatWithHistory.emit(self.__id) + @pyqtSlot() + def on_editButton_clicked(self): + """ + Private slot to edit the chat title. + """ + title, ok = QInputDialog.getText( + self, + self.tr("Edit Chat Title"), + self.tr("Enter the new title:"), + QLineEdit.EchoMode.Normal, + self.__title, + ) + if ok and bool(title): + self.__title = title + self.titleEdit.setText(title) + self.dataChanged.emit(self.__id) + def loadFromJson(self, jsonStr): """ Public method to load the chat history data from a JSON string.
--- a/OllamaInterface/OllamaHistoryWidget.ui Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/OllamaHistoryWidget.ui Tue Aug 06 18:18:39 2024 +0200 @@ -51,6 +51,13 @@ </widget> </item> <item> + <widget class="QToolButton" name="editButton"> + <property name="toolTip"> + <string>Press to edit the chat title.</string> + </property> + </widget> + </item> + <item> <widget class="QToolButton" name="deleteButton"> <property name="toolTip"> <string>Press to delete this chat history.</string> @@ -63,6 +70,7 @@ <tabstop>titleEdit</tabstop> <tabstop>modelEdit</tabstop> <tabstop>newChatButton</tabstop> + <tabstop>editButton</tabstop> <tabstop>deleteButton</tabstop> </tabstops> <resources/>
--- a/OllamaInterface/OllamaWidget.py Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/OllamaWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -9,13 +9,15 @@ import json import os -from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtCore import Qt, QTimer, pyqtSlot from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget from eric7 import Globals from eric7.EricGui import EricPixmapCache from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from .OllamaChatWidget import OllamaChatWidget from .OllamaClient import OllamaClient from .OllamaHistoryWidget import OllamaHistoryWidget from .Ui_OllamaWidget import Ui_OllamaWidget @@ -51,9 +53,16 @@ else: self.layout().setContentsMargins(0, 0, 0, 0) + iconSuffix = "-dark" if ericApp().usesDarkPalette() else "-light" + self.ollamaMenuButton.setIcon(EricPixmapCache.getIcon("superMenu")) self.reloadModelsButton.setIcon(EricPixmapCache.getIcon("reload")) self.newChatButton.setIcon(EricPixmapCache.getIcon("plus")) + self.sendButton.setIcon( + EricPixmapCache.getIcon( + os.path.join("OllamaInterface", "icons", "send{0}".format(iconSuffix)) + ) + ) self.__chatHistoryLayout = QVBoxLayout() self.historyScrollWidget.setLayout(self.__chatHistoryLayout) @@ -66,7 +75,11 @@ self.__connectClient() + self.sendButton.clicked.connect(self.__sendMessage) + self.messageEdit.returnPressed.connect(self.__sendMessage) + self.__loadHistory() + self.__updateMessageEditState() def __connectClient(self): """ @@ -75,6 +88,7 @@ self.__client.serverStateChanged.connect(self.__handleServerStateChanged) self.__client.serverVersion.connect(self.__setHeaderLabel) self.__client.modelsList.connect(self.__populateModelSelector) + self.__client.replyReceived.connect(self.__handleServerMessage) @pyqtSlot(bool) def __handleServerStateChanged(self, ok): @@ -117,30 +131,6 @@ """ self.newChatButton.setEnabled(bool(model)) - @pyqtSlot() - def on_newChatButton_clicked(self): - """ - Private slot to start a new chat with the 'ollama' server. - """ - model = self.modelComboBox.currentText() - if not model: - EricMessageBox.critical( - self, - self.tr("New Chat"), - self.tr("""A model has to be selected first. Aborting..."""), - ) - return - - title, ok = QInputDialog.getText( - self, - self.tr("New Chat"), - self.tr("Enter a title for the new chat:"), - QLineEdit.EchoMode.Normal, - ) - if ok and title: - self.__createHistoryWidget(title, model) - # TODO: create an empty chat widget for new chat - ############################################################################ ## Methods handling signals from the 'ollama' client. ############################################################################ @@ -187,35 +177,46 @@ @param jsonStr string containing JSON serialize chat history data (defaults to None) @type str (optional) + @return reference to the created history widget + @rtype OllamaHistoryWidget """ history = OllamaHistoryWidget(title=title, model=model, jsonStr=jsonStr) self.__chatHistoryLayout.insertWidget( self.__chatHistoryLayout.count() - 1, history ) - scrollbar = self.historyScrollArea.verticalScrollBar() - scrollbar.setMaximum(self.historyScrollWidget.height()) - scrollbar.setValue(scrollbar.maximum()) - history.deleteChatHistory.connect(self.__deleteHistory) history.dataChanged.connect(self.__saveHistory) history.newChatWithHistory.connect(self.__newChatWithHistory) self.__saveHistory() - def __findHistoryWidgetIndex(self, uid): - """ - Private method to find the index of the reference history widget. + QTimer.singleShot(0, self.__scrollHistoryToBottom) + + return history - @param uid ID of the history widget + @pyqtSlot() + def __scrollHistoryToBottom(self): + """ + Private slot to scroll the history widget to the bottom. + """ + scrollbar = self.historyScrollArea.verticalScrollBar() + scrollbar.setMaximum(self.historyScrollWidget.height()) + scrollbar.setValue(scrollbar.maximum()) + + def __findHistoryWidget(self, hid): + """ + Private method to find the widget of a given chat history ID. + + @param hid ID of the chat history @type str - @return index of the history widget - @rtype int + @return reference to the chat history widget + @rtype OllamaHistoryWidget """ for index in range(self.__chatHistoryLayout.count() - 1): widget = self.__chatHistoryLayout.itemAt(index).widget() - if widget.getId() == uid: - return index + if widget.getId() == hid: + return widget return None @@ -237,8 +238,8 @@ entries = {} for index in range(self.__chatHistoryLayout.count() - 1): widget = self.__chatHistoryLayout.itemAt(index).widget() - uid = widget.getId() - entries[uid] = widget.saveToJson() + hid = widget.getId() + entries[hid] = widget.saveToJson() # step 2: save the collected chat histories filePath = self.__historyFilePath() @@ -279,8 +280,8 @@ return # step 2: create history widgets - for uid in entries: - self.__createHistoryWidget("", "", jsonStr=entries[uid]) + for hid in entries: + self.__createHistoryWidget("", "", jsonStr=entries[hid]) def clearHistory(self): """ @@ -295,31 +296,207 @@ self.__saveHistory() @pyqtSlot(str) - def __deleteHistory(self, uid): + def __deleteHistory(self, hid): """ Private slot to delete the history with the given ID. - @param uid ID of the history to be deleted + @param hid ID of the history to be deleted @type str """ - widgetIndex = self.__findHistoryWidgetIndex(uid) - if widgetIndex is not None: + widget = self.__findHistoryWidget(hid) + if widget is not None: + widgetIndex = self.__chatHistoryLayout.indexOf(widget) item = self.__chatHistoryLayout.takeAt(widgetIndex) if item is not None: item.widget().deleteLater() - self.__saveHistory() + self.__saveHistory() + + self.__removeChatWidget(hid) + + ####################################################################### + ## Chat related methods below + ####################################################################### + + def __findChatWidget(self, hid): + """ + Private method to find a chat widget given a chat history ID. + + @param hid chat history ID + @type str + @return reference to the chat widget related to the given ID + @rtype OllamaChatWidget + """ + for index in range(self.chatStackWidget.count()): + widget = self.chatStackWidget.widget(index) + if widget.getHistoryId() == hid: + return widget + + return None + + @pyqtSlot() + def on_newChatButton_clicked(self): + """ + Private slot to start a new chat with the 'ollama' server. + """ + model = self.modelComboBox.currentText() + if not model: + EricMessageBox.critical( + self, + self.tr("New Chat"), + self.tr("""A model has to be selected first. Aborting..."""), + ) + return + + title, ok = QInputDialog.getText( + self, + self.tr("New Chat"), + self.tr("Enter a title for the new chat:"), + QLineEdit.EchoMode.Normal, + ) + if ok and title: + historyWidget = self.__createHistoryWidget(title, model) + hid = historyWidget.getId() + chatWidget = OllamaChatWidget(hid=hid, title=title, model=model) + index = self.chatStackWidget.addWidget(chatWidget) + self.chatStackWidget.setCurrentIndex(index) + + self.__updateMessageEditState() + self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) @pyqtSlot(str) - def __newChatWithHistory(self, uid): + def __newChatWithHistory(self, hid): """ Private slot to start a new chat using a previously saved history. - @param uid ID of the history to be used + @param hid ID of the history to be used + @type str + """ + chatWidget = self.__findChatWidget(hid) + if chatWidget is None: + historyWidget = self.__findHistoryWidget(hid) + if historyWidget is None: + # Oops, treat it as a new chat. + self.on_newChatButton_clicked() + return + + chatWidget = OllamaChatWidget( + hid=hid, title=historyWidget.getTitle(), model=historyWidget.getModel() + ) + index = self.chatStackWidget.addWidget(chatWidget) + self.chatStackWidget.setCurrentIndex(index) + for message in historyWidget.getMessages(): + chatWidget.addMessage(role=message["role"], message=message["content"]) + else: + # simply switch to the already existing chatWidget + self.chatStackWidget.setCurrentWidget(chatWidget) + + self.__updateMessageEditState() + self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) + + def __removeChatWidget(self, hid): + """ + Private method to remove a chat widget given its chat history ID. + + @param hid chat history ID + @type str + """ + widget = self.__findChatWidget(hid) + if widget is not None: + self.chatStackWidget.removeWidget(widget) + + @pyqtSlot() + def __updateMessageEditState(self): + """ + Private slot to set the enabled state of the message line edit and the send + button. + """ + chatActive = bool(self.chatStackWidget.count()) + hasText = bool(self.messageEdit.text()) + + self.messageEdit.setEnabled(chatActive) + self.sendButton.setEnabled(chatActive and hasText) + + @pyqtSlot(str) + def on_messageEdit_textChanged(self, msg): + """ + Private slot to handle a change of the entered message. + + @param msg text of the message line edit @type str """ - # TODO: not implemented yet - pass + self.sendButton.setEnabled(bool(msg)) + + @pyqtSlot() + def __sendMessage(self): + """ + Private method to send the given message of the current chat to the + 'ollama' server. + + This sends the message with context (i.e. the history of the current chat). + """ + msg = self.messageEdit.text() + if not msg: + # empty message => ignore + return + + if not bool(self.chatStackWidget.count()): + # no current stack => ignore + return + + # 1. determine hid of the current chat via chat stack widget + chatWidget = self.chatStackWidget.currentWidget() + hid = chatWidget.getHistoryId() + + # 2. get chat history widget via hid from chat history widget + historyWidget = self.__findHistoryWidget(hid) + if historyWidget is not None: + # 3. append the message to the history + historyWidget.addToMessages("user", msg) + + # 4. get the complete messages list from the history + messages = historyWidget.getMessages() + + # 5. add the message to the current chat and an empty one + # for the response + chatWidget.addMessage("user", msg) + chatWidget.addMessage("assistant", "") + + # 6. send the request via the client (non-streaming (?)) + model = historyWidget.getModel() + self.__client.chat( + model=model, + messages=messages, + streaming=self.__plugin.getPreferences("StreamingChatResponse"), + ) + + # 7. clear the message editor and give input focus back + self.messageEdit.clear() + self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) + + @pyqtSlot(str, str, bool) + def __handleServerMessage(self, content, role, done): + """ + Private slot handling an 'ollama' server chat response. + + @param content message sent by the server + @type str + @param role role name + @type str + @param done flag indicating the last chat response + @type bool + """ + if not bool(self.chatStackWidget.count()): + # no current stack => ignore + return + + chatWidget = self.chatStackWidget.currentWidget() + chatWidget.appendMessage(content) + if done: + hid = chatWidget.getHistoryId() + historyWidget = self.__findHistoryWidget(hid) + if historyWidget is not None: + historyWidget.addToMessages(role, chatWidget.getRecentMessage()) ####################################################################### ## Menu related methods below
--- a/OllamaInterface/OllamaWidget.ui Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/OllamaWidget.ui Tue Aug 06 18:18:39 2024 +0200 @@ -109,7 +109,7 @@ <x>0</x> <y>0</y> <width>533</width> - <height>674</height> + <height>641</height> </rect> </property> </widget> @@ -124,6 +124,30 @@ </widget> </widget> </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLineEdit" name="messageEdit"> + <property name="toolTip"> + <string>Enter the message to be sent to the 'ollama' server.</string> + </property> + <property name="placeholderText"> + <string>Enter Message</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="sendButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to send the message of the current chat to the 'ollama' server.</string> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <customwidgets> @@ -138,6 +162,8 @@ <tabstop>newChatButton</tabstop> <tabstop>reloadModelsButton</tabstop> <tabstop>historyScrollArea</tabstop> + <tabstop>messageEdit</tabstop> + <tabstop>sendButton</tabstop> <tabstop>ollamaMenuButton</tabstop> </tabstops> <resources/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/Ui_OllamaChatWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -0,0 +1,43 @@ +# Form implementation generated from reading ui file 'OllamaInterface/OllamaChatWidget.ui' +# +# Created by: PyQt6 UI code generator 6.7.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_OllamaChatWidget(object): + def setupUi(self, OllamaChatWidget): + OllamaChatWidget.setObjectName("OllamaChatWidget") + OllamaChatWidget.resize(598, 514) + self.verticalLayout = QtWidgets.QVBoxLayout(OllamaChatWidget) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.headerLabel = QtWidgets.QLabel(parent=OllamaChatWidget) + self.headerLabel.setText("Chat Header") + self.headerLabel.setObjectName("headerLabel") + self.horizontalLayout.addWidget(self.headerLabel) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.verticalLayout.addLayout(self.horizontalLayout) + self.chatMessagesScrollArea = QtWidgets.QScrollArea(parent=OllamaChatWidget) + self.chatMessagesScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.chatMessagesScrollArea.setWidgetResizable(True) + self.chatMessagesScrollArea.setObjectName("chatMessagesScrollArea") + self.chatMessagesWidget = QtWidgets.QWidget() + self.chatMessagesWidget.setGeometry(QtCore.QRect(0, 0, 578, 466)) + self.chatMessagesWidget.setObjectName("chatMessagesWidget") + self.chatMessagesScrollArea.setWidget(self.chatMessagesWidget) + self.verticalLayout.addWidget(self.chatMessagesScrollArea) + + self.retranslateUi(OllamaChatWidget) + QtCore.QMetaObject.connectSlotsByName(OllamaChatWidget) + + def retranslateUi(self, OllamaChatWidget): + pass
--- a/OllamaInterface/Ui_OllamaHistoryWidget.py Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/Ui_OllamaHistoryWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -28,6 +28,9 @@ self.newChatButton = QtWidgets.QToolButton(parent=OllamaHistoryWidget) self.newChatButton.setObjectName("newChatButton") self.horizontalLayout.addWidget(self.newChatButton) + self.editButton = QtWidgets.QToolButton(parent=OllamaHistoryWidget) + self.editButton.setObjectName("editButton") + self.horizontalLayout.addWidget(self.editButton) self.deleteButton = QtWidgets.QToolButton(parent=OllamaHistoryWidget) self.deleteButton.setObjectName("deleteButton") self.horizontalLayout.addWidget(self.deleteButton) @@ -36,9 +39,11 @@ QtCore.QMetaObject.connectSlotsByName(OllamaHistoryWidget) OllamaHistoryWidget.setTabOrder(self.titleEdit, self.modelEdit) OllamaHistoryWidget.setTabOrder(self.modelEdit, self.newChatButton) - OllamaHistoryWidget.setTabOrder(self.newChatButton, self.deleteButton) + OllamaHistoryWidget.setTabOrder(self.newChatButton, self.editButton) + OllamaHistoryWidget.setTabOrder(self.editButton, self.deleteButton) def retranslateUi(self, OllamaHistoryWidget): _translate = QtCore.QCoreApplication.translate self.newChatButton.setToolTip(_translate("OllamaHistoryWidget", "Press to start a new chat based on the current history.")) + self.editButton.setToolTip(_translate("OllamaHistoryWidget", "Press to edit the chat title.")) self.deleteButton.setToolTip(_translate("OllamaHistoryWidget", "Press to delete this chat history."))
--- a/OllamaInterface/Ui_OllamaWidget.py Mon Aug 05 18:37:16 2024 +0200 +++ b/OllamaInterface/Ui_OllamaWidget.py Tue Aug 06 18:18:39 2024 +0200 @@ -60,7 +60,7 @@ self.historyScrollArea.setWidgetResizable(True) self.historyScrollArea.setObjectName("historyScrollArea") self.historyScrollWidget = QtWidgets.QWidget() - self.historyScrollWidget.setGeometry(QtCore.QRect(0, 0, 533, 674)) + self.historyScrollWidget.setGeometry(QtCore.QRect(0, 0, 533, 641)) self.historyScrollWidget.setObjectName("historyScrollWidget") self.historyScrollArea.setWidget(self.historyScrollWidget) self.chatStackWidget = QtWidgets.QStackedWidget(parent=self.mainSplitter) @@ -71,17 +71,32 @@ self.chatStackWidget.setSizePolicy(sizePolicy) self.chatStackWidget.setObjectName("chatStackWidget") self.verticalLayout.addWidget(self.mainSplitter) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.messageEdit = QtWidgets.QLineEdit(parent=OllamaWidget) + self.messageEdit.setObjectName("messageEdit") + self.horizontalLayout_3.addWidget(self.messageEdit) + self.sendButton = QtWidgets.QToolButton(parent=OllamaWidget) + self.sendButton.setEnabled(False) + self.sendButton.setObjectName("sendButton") + self.horizontalLayout_3.addWidget(self.sendButton) + self.verticalLayout.addLayout(self.horizontalLayout_3) self.retranslateUi(OllamaWidget) QtCore.QMetaObject.connectSlotsByName(OllamaWidget) OllamaWidget.setTabOrder(self.modelComboBox, self.newChatButton) OllamaWidget.setTabOrder(self.newChatButton, self.reloadModelsButton) OllamaWidget.setTabOrder(self.reloadModelsButton, self.historyScrollArea) - OllamaWidget.setTabOrder(self.historyScrollArea, self.ollamaMenuButton) + OllamaWidget.setTabOrder(self.historyScrollArea, self.messageEdit) + OllamaWidget.setTabOrder(self.messageEdit, self.sendButton) + OllamaWidget.setTabOrder(self.sendButton, self.ollamaMenuButton) def retranslateUi(self, OllamaWidget): _translate = QtCore.QCoreApplication.translate self.reloadModelsButton.setStatusTip(_translate("OllamaWidget", "Select to reload the list of selectable models.")) self.modelComboBox.setStatusTip(_translate("OllamaWidget", "Select the model for the chat.")) self.newChatButton.setToolTip(_translate("OllamaWidget", "Press to start a new chat.")) + self.messageEdit.setToolTip(_translate("OllamaWidget", "Enter the message to be sent to the \'ollama\' server.")) + self.messageEdit.setPlaceholderText(_translate("OllamaWidget", "Enter Message")) + self.sendButton.setToolTip(_translate("OllamaWidget", "Press to send the message of the current chat to the \'ollama\' server.")) from eric7.EricWidgets.EricToolButton import EricToolButton
--- a/PluginAiOllama.epj Mon Aug 05 18:37:16 2024 +0200 +++ b/PluginAiOllama.epj Tue Aug 06 18:18:39 2024 +0200 @@ -198,6 +198,7 @@ "makefile": "OTHERS" }, "FORMS": [ + "OllamaInterface/OllamaChatWidget.ui", "OllamaInterface/OllamaHistoryWidget.ui", "OllamaInterface/OllamaWidget.ui" ], @@ -290,9 +291,13 @@ }, "RESOURCES": [], "SOURCES": [ + "OllamaInterface/AutoResizeTextBrowser.py", + "OllamaInterface/OllamaChatMessageBox.py", + "OllamaInterface/OllamaChatWidget.py", "OllamaInterface/OllamaClient.py", "OllamaInterface/OllamaHistoryWidget.py", "OllamaInterface/OllamaWidget.py", + "OllamaInterface/Ui_OllamaChatWidget.py", "OllamaInterface/Ui_OllamaHistoryWidget.py", "OllamaInterface/Ui_OllamaWidget.py", "OllamaInterface/__init__.py",
--- a/PluginAiOllama.py Mon Aug 05 18:37:16 2024 +0200 +++ b/PluginAiOllama.py Tue Aug 06 18:18:39 2024 +0200 @@ -12,7 +12,7 @@ from PyQt6.QtCore import QObject, Qt, QTranslator, pyqtSignal from PyQt6.QtGui import QKeySequence -from eric7 import Preferences +from eric7 import Globals, Preferences from eric7.EricGui import EricPixmapCache from eric7.EricGui.EricAction import EricAction from eric7.EricWidgets.EricApplication import ericApp @@ -128,6 +128,7 @@ "OllamaHost": "localhost", "OllamaPort": 11434, "OllamaHeartbeatInterval": 5, # 5 seconds heartbeat time; 0 = disabled + "StreamingChatResponse": True, } self.__translator = None @@ -273,6 +274,12 @@ self.PreferencesKey + "/" + key, self.__defaults[key] ) ) + elif key in ("StreamingChatResponse",): + return Globals.toBool( + Preferences.Prefs.settings.value( + self.PreferencesKey + "/" + key, self.__defaults[key] + ) + ) else: return Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key]