Implemented most of the Chat History widgets.

Mon, 05 Aug 2024 18:37:16 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 05 Aug 2024 18:37:16 +0200
changeset 4
7dd1b9cd3150
parent 3
ca28466a186d
child 5
6e8af43d537d

Implemented most of the Chat History widgets.

OllamaInterface/OllamaClient.py file | annotate | diff | comparison | revisions
OllamaInterface/OllamaHistoryWidget.py file | annotate | diff | comparison | revisions
OllamaInterface/OllamaHistoryWidget.ui file | annotate | diff | comparison | revisions
OllamaInterface/OllamaWidget.py file | annotate | diff | comparison | revisions
OllamaInterface/OllamaWidget.ui file | annotate | diff | comparison | revisions
OllamaInterface/Ui_OllamaHistoryWidget.py file | annotate | diff | comparison | revisions
OllamaInterface/Ui_OllamaWidget.py file | annotate | diff | comparison | revisions
PluginAiOllama.epj file | annotate | diff | comparison | revisions
PluginAiOllama.py file | annotate | diff | comparison | revisions
--- a/OllamaInterface/OllamaClient.py	Sun Aug 04 16:57:01 2024 +0200
+++ b/OllamaInterface/OllamaClient.py	Mon Aug 05 18:37:16 2024 +0200
@@ -12,8 +12,16 @@
 import enum
 import json
 
-from PyQt6.QtCore import pyqtSignal, QObject, QUrl
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
+from PyQt6.QtCore import (
+    QCoreApplication,
+    QObject,
+    QThread,
+    QTimer,
+    QUrl,
+    pyqtSignal,
+    pyqtSlot,
+)
+from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
 
 from eric7.EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired
 
@@ -22,6 +30,7 @@
     """
     Class defining the various client states.
     """
+
     Waiting = 0
     Requesting = 1
     Receiving = 2
@@ -32,7 +41,7 @@
     """
     Class implementing the 'ollama' client.
 
-    @signal replyReceived(content:str, role:str) emitted after a response from the 
+    @signal replyReceived(content:str, role:str) 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
@@ -43,9 +52,13 @@
         execution details
     @signal pullStatus(msg:str, id:str, total:int, completed:int) emitted to indicate
         the status of a pull request as reported by the 'ollama' server
+    @signal serverVersion(version:str) emitted after the server version was obtained
+        from the 'ollama' server
     @signal finished() emitted to indicate the completion of a request
     @signal errorOccurred(error:str) emitted to indicate a network error occurred
         while processing the request
+    @signal serverStateChanged(ok:bool) emitted to indicate a change of the server
+        responsiveness
     """
 
     replyReceived = pyqtSignal(str, str)
@@ -53,8 +66,10 @@
     detailedModelsList = pyqtSignal(list)
     runningModelsList = pyqtSignal(list)
     pullStatus = pyqtSignal(str, str, int, int)
+    serverVersion = pyqtSignal(str)
     finished = pyqtSignal()
     errorOccurred = pyqtSignal(str)
+    serverStateChanged = pyqtSignal(bool)
 
     def __init__(self, plugin, parent=None):
         """
@@ -75,8 +90,17 @@
             proxyAuthenticationRequired
         )
 
+        self.__serverResponding = False
+        self.__heartbeatTimer = QTimer(self)
+        self.__heartbeatTimer.timeout.connect(self.__periodicHeartbeat)
+
         self.__state = OllamaClientState.Waiting
 
+        self.__serverResponding = False  # start with a faulty state
+
+        self.__plugin.preferencesChanged.connect(self.__setHeartbeatTimer)
+        self.__setHeartbeatTimer()
+
     def chat(self, model, messages):
         """
         Public method to request a chat completion from the 'ollama' server.
@@ -188,7 +212,6 @@
         Public method to request a list of models available locally from the 'ollama'
         server.
         """
-        # TODO: not implemented yet
         self.__sendRequest("tags", processResponse=self.__processModelsList)
 
     def __processModelsList(self, response):
@@ -203,16 +226,13 @@
             for model in response["models"]:
                 name = model["name"]
                 if name:
-                    models.append(name)
+                    models.append(name.replace(":latest", ""))
         self.modelsList.emit(models)
 
     def listDetails(self):
         """
         Public method to request a list of models available locally from the 'ollama'
         server with some model details.
-
-        @return list of available models
-        @rtype list of dict
         """
         # TODO: not implemented yet
         self.__sendRequest("tags", processResponse=self.__processDetailedModelsList)
@@ -244,7 +264,7 @@
 
     def listRunning(self):
         """
-        Public method to request a list of running models from the 'ollama' server
+        Public method to request a list of running models from the 'ollama' server.
         """
         # TODO: not implemented yet
         self.__sendRequest("ps", processResponse=self.__processRunningModelsList)
@@ -288,10 +308,26 @@
                     )
         self.runningModelsList.emit(models)
 
+    def version(self):
+        """
+        Public method to request the version from the 'ollama' server.
+        """
+        self.__sendRequest("version", processResponse=self.__processVersion)
+
+    def __processVersion(self, response):
+        """
+        Private method to process the version response of the 'ollama' server.
+
+        @param response dictionary containing the version response
+        @type dict
+        """
+        with contextlib.suppress(KeyError):
+            self.serverVersion.emit(response["version"])
+
     def state(self):
         """
         Public method to get the current client state.
-        
+
         @return current client state
         @rtype OllamaClientState
         """
@@ -326,9 +362,9 @@
                 QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/json"
             )
             jsonData = json.dumps(data).encode("utf-8")
-            reply = self.__networkManager.post(request=request, data=jsonData)
+            reply = self.__networkManager.post(request, jsonData)
         else:
-            reply = self.__networkManager.get(request=request)
+            reply = self.__networkManager.get(request)
 
         reply.finished.connect(lambda: self.__replyFinished(reply))
         reply.errorOccurred.connect(lambda error: self.__errorOccurred(error, reply))
@@ -383,3 +419,49 @@
                 data = json.loads(buffer)
                 if data and processResponse:
                     processResponse(data)
+
+    def heartbeat(self):
+        """
+        Public method to check, if the 'ollama' server has started and is responsive.
+
+        @return flag indicating a responsive 'ollama' server
+        @rtype bool
+        """
+        ollamaUrl = QUrl(
+            "{0}://{1}:{2}/".format(
+                self.__plugin.getPreferences("OllamaScheme"),
+                self.__plugin.getPreferences("OllamaHost"),
+                self.__plugin.getPreferences("OllamaPort"),
+            )
+        )
+        request = QNetworkRequest(ollamaUrl)
+        reply = self.__networkManager.head(request)
+        while not reply.isFinished():
+            QCoreApplication.processEvents()
+            QThread.msleep(100)
+
+        reply.deleteLater()
+
+        return reply.error() == QNetworkReply.NetworkError.NoError
+
+    @pyqtSlot()
+    def __setHeartbeatTimer(self):
+        """
+        Private slot to configure the heartbeat timer.
+        """
+        interval = self.__plugin.getPreferences("OllamaHeartbeatInterval")
+        if interval:
+            self.__heartbeatTimer.setInterval(interval * 1000)  # interval in ms
+            self.__heartbeatTimer.start()
+        else:
+            self.__heartbeatTimer.stop()
+
+    @pyqtSlot()
+    def __periodicHeartbeat(self):
+        """
+        Private slot to do a periodic check of the 'ollama' server responsiveness.
+        """
+        responding = self.heartbeat()
+        if responding != self.__serverResponding:
+            self.serverStateChanged.emit(responding)
+        self.__serverResponding = responding
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/OllamaHistoryWidget.py	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a widget showing a chat title and store a chat contents.
+"""
+
+import json
+import uuid
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot
+from PyQt6.QtWidgets import QWidget
+
+from eric7.EricGui import EricPixmapCache
+
+from .Ui_OllamaHistoryWidget import Ui_OllamaHistoryWidget
+
+
+class OllamaHistoryWidget(QWidget, Ui_OllamaHistoryWidget):
+    """
+    Class implementing a widget showing a chat title and store a chat contents.
+
+    @signal deleteChatHistory(id:str) emitted to indicate, that this chat history
+        should be deleted
+    @signal newChatWithHistory(id:str) emitted to indicate, that a new chat using
+        the saved history should be started
+    @signal dataChanged(id:str) emitted to indicate a change of the chat history data
+    """
+
+    deleteChatHistory = pyqtSignal(str)
+    newChatWithHistory = pyqtSignal(str)
+    dataChanged = pyqtSignal(str)
+
+    def __init__(self, title, model, jsonStr=None, parent=None):
+        """
+        Constructor
+
+        @param title title of the ollama chat
+        @type str
+        @param model name of the model used for the chat
+        @type str
+        @param jsonStr string containing JSON serialize chat history data
+            (defaults to None)
+        @type str (optional)
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.newChatButton.setIcon(EricPixmapCache.getIcon("plus"))
+        self.deleteButton.setIcon(EricPixmapCache.getIcon("trash"))
+
+        if jsonStr is None:
+            self.__title = title
+            self.__model = model
+
+            self.__id = str(uuid.uuid4())
+            self.__messages = []
+        else:
+            self.loadFromJson(jsonStr)
+
+        self.titleEdit.setText(self.__title)
+        self.modelEdit.setText(self.__model)
+
+    def getId(self):
+        """
+        Public method to get the chat history ID.
+        
+        @return ID of the history entry
+        @rtype str
+        """
+        return self.__id
+
+    @pyqtSlot()
+    def on_deleteButton_clicked(self):
+        """
+        Private slot to delet this chat history entry..
+        """
+        self.deleteChatHistory.emit(self.__id)
+
+    @pyqtSlot()
+    def on_newChatButton_clicked(self):
+        """
+        Private slot to start a new chat using the saved chat history.
+        """
+        self.newChatWithHistory.emit(self.__id)
+
+    def loadFromJson(self, jsonStr):
+        """
+        Public method to load the chat history data from a JSON string.
+
+        @param jsonStr JSON serialized chat data
+        @type str
+        """
+        data = json.loads(jsonStr)
+        self.__id = data["id"]
+        self.__title = data["title"]
+        self.__model = data["model"]
+        self.__messages = data["messages"]
+
+    def saveToJson(self):
+        """
+        Public method to serialize the chat history data to a JSON string.
+
+        @return JSON serialized chat data
+        @rtype str
+        """
+        return json.dumps(
+            {
+                "id": self.__id,
+                "title": self.__title,
+                "model": self.__model,
+                "messages": self.__messages,
+            }
+        )
+
+    def addToMessages(self, role, content):
+        """
+        Public method to add a chat message to the chat history.
+
+        @param role chat role (one of 'system', 'user', 'assistant' or 'tool')
+        @type str
+        @param content content of the chat message
+        @type str
+        """
+        self.__messages.append(
+            {
+                "role": role,
+                "content": content,
+            }
+        )
+        self.dataChanged.emit(self.__id)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/OllamaHistoryWidget.ui	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OllamaHistoryWidget</class>
+ <widget class="QWidget" name="OllamaHistoryWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>25</height>
+   </rect>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QLineEdit" name="titleEdit">
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="modelEdit">
+     <property name="maximumSize">
+      <size>
+       <width>100</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QToolButton" name="newChatButton">
+     <property name="toolTip">
+      <string>Press to start a new chat based on the current history.</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QToolButton" name="deleteButton">
+     <property name="toolTip">
+      <string>Press to delete this chat history.</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>titleEdit</tabstop>
+  <tabstop>modelEdit</tabstop>
+  <tabstop>newChatButton</tabstop>
+  <tabstop>deleteButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/OllamaWidget.py	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,339 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the main ollama interface widget.
+"""
+
+import json
+import os
+
+from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget
+
+from eric7 import Globals
+from eric7.EricGui import EricPixmapCache
+from eric7.EricWidgets import EricMessageBox
+
+from .OllamaClient import OllamaClient
+from .OllamaHistoryWidget import OllamaHistoryWidget
+from .Ui_OllamaWidget import Ui_OllamaWidget
+
+
+class OllamaWidget(QWidget, Ui_OllamaWidget):
+    """
+    Class implementing the main ollama interface widget.
+    """
+
+    OllamaHistoryFile = "ollama_history.json"
+
+    def __init__(self, plugin, fromEric=True, parent=None):
+        """
+        Constructor
+
+        @param plugin reference to the plug-in object
+        @type PluginOllamaInterface
+        @param fromEric flag indicating the eric-ide mode (defaults to True)
+            (True = eric-ide mode, False = application mode)
+        @type bool (optional)
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.__plugin = plugin
+        self.__client = OllamaClient(plugin, self)
+
+        if fromEric:
+            self.layout().setContentsMargins(0, 3, 0, 0)
+        else:
+            self.layout().setContentsMargins(0, 0, 0, 0)
+
+        self.ollamaMenuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
+        self.reloadModelsButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.newChatButton.setIcon(EricPixmapCache.getIcon("plus"))
+
+        self.__chatHistoryLayout = QVBoxLayout()
+        self.historyScrollWidget.setLayout(self.__chatHistoryLayout)
+        self.__chatHistoryLayout.addStretch(1)
+
+        self.mainSplitter.setSizes([200, 2000])
+
+        self.newChatButton.setEnabled(False)
+        self.__handleServerStateChanged(False)
+
+        self.__connectClient()
+
+        self.__loadHistory()
+
+    def __connectClient(self):
+        """
+        Private method to connect the client signals.
+        """
+        self.__client.serverStateChanged.connect(self.__handleServerStateChanged)
+        self.__client.serverVersion.connect(self.__setHeaderLabel)
+        self.__client.modelsList.connect(self.__populateModelSelector)
+
+    @pyqtSlot(bool)
+    def __handleServerStateChanged(self, ok):
+        """
+        Private slot handling a change in the 'ollama' server responsiveness.
+
+        @param ok flag indicating a responsive 'ollama' server
+        @type bool
+        """
+        if ok:
+            self.__finishSetup()
+        else:
+            self.ollamaVersionLabel.setText(
+                self.tr("<b>Error: The configured server is not responding.</b>")
+            )
+        self.setEnabled(ok)
+
+    @pyqtSlot()
+    def __finishSetup(self):
+        """
+        Private slot to finish the UI setup.
+        """
+        self.__client.version()
+        self.__client.list()
+
+    @pyqtSlot()
+    def on_reloadModelsButton_clicked(self):
+        """
+        Private slot to reload the list of available models.
+        """
+        self.__client.list()
+
+    @pyqtSlot(str)
+    def on_modelComboBox_currentTextChanged(self, model):
+        """
+        Private slot handling the selection of a model.
+
+        @param model name of the selected model
+        @type str
+        """
+        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.
+    ############################################################################
+
+    @pyqtSlot(str)
+    def __setHeaderLabel(self, version):
+        """
+        Private slot to receive the 'ollama' server version and set the header.
+
+        @param version 'ollama' server version'
+        @type str
+        """
+        self.ollamaVersionLabel.setText(
+            self.tr("<b>ollama Server Version {0}</b>").format(version)
+        )
+
+    @pyqtSlot(list)
+    def __populateModelSelector(self, modelNames):
+        """
+        Private slot to receive the list of available model names and populate
+        the model selector with them.
+
+        @param modelNames list of model names
+        @type list[str]
+        """
+        self.modelComboBox.clear()
+
+        self.modelComboBox.addItem("")
+        self.modelComboBox.addItems(sorted(modelNames))
+
+    ############################################################################
+    ## Methods handling signals from the chat history widgets.
+    ############################################################################
+
+    def __createHistoryWidget(self, title, model, jsonStr=None):
+        """
+        Private method to create a chat history widget and insert it into the
+        respective layout.
+
+        @param title title of the chat
+        @type str
+        @param model name of the model
+        @type str
+        @param jsonStr string containing JSON serialize chat history data (defaults
+            to None)
+        @type str (optional)
+        """
+        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.
+
+        @param uid ID of the history widget
+        @type str
+        @return index of the history widget
+        @rtype int
+        """
+        for index in range(self.__chatHistoryLayout.count() - 1):
+            widget = self.__chatHistoryLayout.itemAt(index).widget()
+            if widget.getId() == uid:
+                return index
+
+        return None
+
+    def __historyFilePath(self):
+        """
+        Private method to get the path name of the chat history file.
+
+        @return file path of the chat history file
+        @rtype str
+        """
+        return os.path.join(Globals.getConfigDir(), OllamaWidget.OllamaHistoryFile)
+
+    @pyqtSlot()
+    def __saveHistory(self):
+        """
+        Private method to save the current chat history to the history file.
+        """
+        # step 1: collect all history entries
+        entries = {}
+        for index in range(self.__chatHistoryLayout.count() - 1):
+            widget = self.__chatHistoryLayout.itemAt(index).widget()
+            uid = widget.getId()
+            entries[uid] = widget.saveToJson()
+
+        # step 2: save the collected chat histories
+        filePath = self.__historyFilePath()
+        try:
+            with open(filePath, "w") as f:
+                json.dump(entries, f)
+        except OSError as err:
+            EricMessageBox.critical(
+                self,
+                self.tr("Save Chat History"),
+                self.tr(
+                    "<p>The chat history could not be saved to <b>{0}</b>.</p>"
+                    "<p>Reason: {1}</p>"
+                ).format(filePath, str(err)),
+            )
+
+    def __loadHistory(self):
+        """
+        Private method to load a previously saved history file.
+        """
+        # step 1: load the history file, if it exists
+        filePath = self.__historyFilePath()
+        if not os.path.exists(filePath):
+            return
+
+        try:
+            with open(filePath, "r") as f:
+                entries = json.load(f)
+        except OSError as err:
+            EricMessageBox.critical(
+                self,
+                self.tr("Load Chat History"),
+                self.tr(
+                    "<p>The chat history could not be loaded from <b>{0}</b>.</p>"
+                    "<p>Reason: {1}</p>"
+                ).format(filePath, str(err)),
+            )
+            return
+
+        # step 2: create history widgets
+        for uid in entries:
+            self.__createHistoryWidget("", "", jsonStr=entries[uid])
+
+    def clearHistory(self):
+        """
+        Public method to clear the history entries and close all chats.
+        """
+        while self.__chatHistoryLayout.count() > 1:
+            # do not delete the spacer at the end of the list
+            item = self.__chatHistoryLayout.takeAt(0)
+            if item is not None:
+                item.widget().deleteLater()
+
+        self.__saveHistory()
+
+    @pyqtSlot(str)
+    def __deleteHistory(self, uid):
+        """
+        Private slot to delete the history with the given ID.
+
+        @param uid ID of the history to be deleted
+        @type str
+        """
+        widgetIndex = self.__findHistoryWidgetIndex(uid)
+        if widgetIndex is not None:
+            item = self.__chatHistoryLayout.takeAt(widgetIndex)
+            if item is not None:
+                item.widget().deleteLater()
+
+        self.__saveHistory()
+
+    @pyqtSlot(str)
+    def __newChatWithHistory(self, uid):
+        """
+        Private slot to start a new chat using a previously saved history.
+
+        @param uid ID of the history to be used
+        @type str
+        """
+        # TODO: not implemented yet
+        pass
+
+    #######################################################################
+    ## Menu related methods below
+    #######################################################################
+
+    def __initOllamaMenu(self):
+        """
+        Private method to create the super menu and attach it to the super
+        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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/OllamaWidget.ui	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OllamaWidget</class>
+ <widget class="QWidget" name="OllamaWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>553</width>
+    <height>762</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <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="ollamaVersionLabel"/>
+     </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>
+     <item>
+      <widget class="EricToolButton" name="ollamaMenuButton">
+       <property name="popupMode">
+        <enum>QToolButton::InstantPopup</enum>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QToolButton" name="reloadModelsButton">
+       <property name="statusTip">
+        <string>Select to reload the list of selectable models.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="modelComboBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="statusTip">
+        <string>Select the model for the chat.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="newChatButton">
+       <property name="toolTip">
+        <string>Press to start a new chat.</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QSplitter" name="mainSplitter">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="childrenCollapsible">
+      <bool>false</bool>
+     </property>
+     <widget class="QScrollArea" name="historyScrollArea">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="horizontalScrollBarPolicy">
+       <enum>Qt::ScrollBarAlwaysOff</enum>
+      </property>
+      <property name="widgetResizable">
+       <bool>true</bool>
+      </property>
+      <widget class="QWidget" name="historyScrollWidget">
+       <property name="geometry">
+        <rect>
+         <x>0</x>
+         <y>0</y>
+         <width>533</width>
+         <height>674</height>
+        </rect>
+       </property>
+      </widget>
+     </widget>
+     <widget class="QStackedWidget" name="chatStackWidget">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>EricToolButton</class>
+   <extends>QToolButton</extends>
+   <header>eric7/EricWidgets/EricToolButton.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>modelComboBox</tabstop>
+  <tabstop>newChatButton</tabstop>
+  <tabstop>reloadModelsButton</tabstop>
+  <tabstop>historyScrollArea</tabstop>
+  <tabstop>ollamaMenuButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/Ui_OllamaHistoryWidget.py	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,44 @@
+# Form implementation generated from reading ui file 'OllamaInterface/OllamaHistoryWidget.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_OllamaHistoryWidget(object):
+    def setupUi(self, OllamaHistoryWidget):
+        OllamaHistoryWidget.setObjectName("OllamaHistoryWidget")
+        OllamaHistoryWidget.resize(400, 25)
+        self.horizontalLayout = QtWidgets.QHBoxLayout(OllamaHistoryWidget)
+        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        self.titleEdit = QtWidgets.QLineEdit(parent=OllamaHistoryWidget)
+        self.titleEdit.setReadOnly(True)
+        self.titleEdit.setObjectName("titleEdit")
+        self.horizontalLayout.addWidget(self.titleEdit)
+        self.modelEdit = QtWidgets.QLineEdit(parent=OllamaHistoryWidget)
+        self.modelEdit.setMaximumSize(QtCore.QSize(100, 16777215))
+        self.modelEdit.setReadOnly(True)
+        self.modelEdit.setObjectName("modelEdit")
+        self.horizontalLayout.addWidget(self.modelEdit)
+        self.newChatButton = QtWidgets.QToolButton(parent=OllamaHistoryWidget)
+        self.newChatButton.setObjectName("newChatButton")
+        self.horizontalLayout.addWidget(self.newChatButton)
+        self.deleteButton = QtWidgets.QToolButton(parent=OllamaHistoryWidget)
+        self.deleteButton.setObjectName("deleteButton")
+        self.horizontalLayout.addWidget(self.deleteButton)
+
+        self.retranslateUi(OllamaHistoryWidget)
+        QtCore.QMetaObject.connectSlotsByName(OllamaHistoryWidget)
+        OllamaHistoryWidget.setTabOrder(self.titleEdit, self.modelEdit)
+        OllamaHistoryWidget.setTabOrder(self.modelEdit, self.newChatButton)
+        OllamaHistoryWidget.setTabOrder(self.newChatButton, 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.deleteButton.setToolTip(_translate("OllamaHistoryWidget", "Press to delete this chat history."))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OllamaInterface/Ui_OllamaWidget.py	Mon Aug 05 18:37:16 2024 +0200
@@ -0,0 +1,87 @@
+# Form implementation generated from reading ui file 'OllamaInterface/OllamaWidget.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_OllamaWidget(object):
+    def setupUi(self, OllamaWidget):
+        OllamaWidget.setObjectName("OllamaWidget")
+        OllamaWidget.resize(553, 762)
+        self.verticalLayout = QtWidgets.QVBoxLayout(OllamaWidget)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
+        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem)
+        self.ollamaVersionLabel = QtWidgets.QLabel(parent=OllamaWidget)
+        self.ollamaVersionLabel.setObjectName("ollamaVersionLabel")
+        self.horizontalLayout_2.addWidget(self.ollamaVersionLabel)
+        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem1)
+        self.ollamaMenuButton = EricToolButton(parent=OllamaWidget)
+        self.ollamaMenuButton.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)
+        self.ollamaMenuButton.setObjectName("ollamaMenuButton")
+        self.horizontalLayout_2.addWidget(self.ollamaMenuButton)
+        self.verticalLayout.addLayout(self.horizontalLayout_2)
+        self.horizontalLayout = QtWidgets.QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        self.reloadModelsButton = QtWidgets.QToolButton(parent=OllamaWidget)
+        self.reloadModelsButton.setObjectName("reloadModelsButton")
+        self.horizontalLayout.addWidget(self.reloadModelsButton)
+        self.modelComboBox = QtWidgets.QComboBox(parent=OllamaWidget)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(self.modelComboBox.sizePolicy().hasHeightForWidth())
+        self.modelComboBox.setSizePolicy(sizePolicy)
+        self.modelComboBox.setObjectName("modelComboBox")
+        self.horizontalLayout.addWidget(self.modelComboBox)
+        self.newChatButton = QtWidgets.QToolButton(parent=OllamaWidget)
+        self.newChatButton.setObjectName("newChatButton")
+        self.horizontalLayout.addWidget(self.newChatButton)
+        self.verticalLayout.addLayout(self.horizontalLayout)
+        self.mainSplitter = QtWidgets.QSplitter(parent=OllamaWidget)
+        self.mainSplitter.setOrientation(QtCore.Qt.Orientation.Vertical)
+        self.mainSplitter.setChildrenCollapsible(False)
+        self.mainSplitter.setObjectName("mainSplitter")
+        self.historyScrollArea = QtWidgets.QScrollArea(parent=self.mainSplitter)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(self.historyScrollArea.sizePolicy().hasHeightForWidth())
+        self.historyScrollArea.setSizePolicy(sizePolicy)
+        self.historyScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        self.historyScrollArea.setWidgetResizable(True)
+        self.historyScrollArea.setObjectName("historyScrollArea")
+        self.historyScrollWidget = QtWidgets.QWidget()
+        self.historyScrollWidget.setGeometry(QtCore.QRect(0, 0, 533, 674))
+        self.historyScrollWidget.setObjectName("historyScrollWidget")
+        self.historyScrollArea.setWidget(self.historyScrollWidget)
+        self.chatStackWidget = QtWidgets.QStackedWidget(parent=self.mainSplitter)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(self.chatStackWidget.sizePolicy().hasHeightForWidth())
+        self.chatStackWidget.setSizePolicy(sizePolicy)
+        self.chatStackWidget.setObjectName("chatStackWidget")
+        self.verticalLayout.addWidget(self.mainSplitter)
+
+        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)
+
+    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."))
+from eric7.EricWidgets.EricToolButton import EricToolButton
--- a/PluginAiOllama.epj	Sun Aug 04 16:57:01 2024 +0200
+++ b/PluginAiOllama.epj	Mon Aug 05 18:37:16 2024 +0200
@@ -5,7 +5,149 @@
   },
   "project": {
     "AUTHOR": "Detlev Offenbach",
-    "CHECKERSPARMS": {},
+    "CHECKERSPARMS": {
+      "Pep8Checker": {
+        "AnnotationsChecker": {
+          "AllowStarArgAny": false,
+          "AllowUntypedDefs": false,
+          "AllowUntypedNested": false,
+          "CheckFutureAnnotations": false,
+          "DispatchDecorators": [
+            "singledispatch",
+            "singledispatchmethod"
+          ],
+          "ExemptedTypingSymbols": [
+            ""
+          ],
+          "ForceFutureAnnotations": false,
+          "MaximumComplexity": 3,
+          "MaximumLength": 7,
+          "MinimumCoverage": 75,
+          "MypyInitReturn": false,
+          "OverloadDecorators": [
+            "overload"
+          ],
+          "RespectTypeIgnore": false,
+          "SuppressDummyArgs": false,
+          "SuppressNoneReturning": true
+        },
+        "BlankLines": [
+          2,
+          1
+        ],
+        "BuiltinsChecker": {
+          "bytes": [
+            "unicode"
+          ],
+          "chr": [
+            "unichr"
+          ],
+          "str": [
+            "unicode"
+          ]
+        },
+        "CommentedCodeChecker": {
+          "Aggressive": false,
+          "WhiteList": [
+            "pylint",
+            "pyright",
+            "noqa",
+            "type:\\s*ignore",
+            "fmt:\\s*(on|off)",
+            "TODO",
+            "FIXME",
+            "WARNING",
+            "NOTE",
+            "TEST",
+            "DOCU",
+            "XXX",
+            "- "
+          ]
+        },
+        "CopyrightAuthor": "",
+        "CopyrightMinFileSize": 0,
+        "DocstringType": "eric_black",
+        "EnabledCheckerCategories": "ASY, C, D, E, I, L, M, NO, N, Y, U, W",
+        "ExcludeFiles": "*/Ui_*.py",
+        "ExcludeMessages": "M201,C101,E203,E265,E266,E305,E402,M251,M701,M702,M811,M834,M852,N802,N803,N807,N808,N821,U101,W293,W503,Y119,Y401,Y402",
+        "FixCodes": "",
+        "FixIssues": false,
+        "FutureChecker": "",
+        "HangClosing": false,
+        "ImportsChecker": {
+          "ApplicationPackageNames": [
+            "OllamaInterface",
+            "eric7"
+          ],
+          "BanRelativeImports": "",
+          "BannedModules": []
+        },
+        "IncludeMessages": "",
+        "LineComplexity": 25,
+        "LineComplexityScore": 10,
+        "MaxCodeComplexity": 10,
+        "MaxDocLineLength": 88,
+        "MaxLineLength": 88,
+        "NameOrderChecker": {
+          "ApplicationPackageNames": [
+            "OllamaInterface",
+            "eric7"
+          ],
+          "CombinedAsImports": true,
+          "SortCaseSensitive": false,
+          "SortFromFirst": false,
+          "SortIgnoringStyle": false,
+          "SortOrder": "natural"
+        },
+        "NoFixCodes": "E501",
+        "RepeatMessages": true,
+        "SecurityChecker": {
+          "CheckTypedException": false,
+          "HardcodedTmpDirectories": [
+            "/tmp",
+            "/var/tmp",
+            "/dev/shm",
+            "~/tmp"
+          ],
+          "InsecureHashes": [
+            "md4",
+            "md5",
+            "sha",
+            "sha1"
+          ],
+          "InsecureSslProtocolVersions": [
+            "PROTOCOL_SSLv2",
+            "SSLv2_METHOD",
+            "SSLv23_METHOD",
+            "PROTOCOL_SSLv3",
+            "PROTOCOL_TLSv1",
+            "SSLv3_METHOD",
+            "TLSv1_METHOD"
+          ],
+          "WeakKeySizeDsaHigh": "1024",
+          "WeakKeySizeDsaMedium": "2048",
+          "WeakKeySizeEcHigh": "160",
+          "WeakKeySizeEcMedium": "224",
+          "WeakKeySizeRsaHigh": "1024",
+          "WeakKeySizeRsaMedium": "2048"
+        },
+        "ShowIgnored": false,
+        "UnusedChecker": {
+          "IgnoreAbstract": true,
+          "IgnoreDunderGlobals": true,
+          "IgnoreDunderMethods": true,
+          "IgnoreEventHandlerMethods": true,
+          "IgnoreLambdas": false,
+          "IgnoreNestedFunctions": false,
+          "IgnoreOverload": true,
+          "IgnoreOverride": true,
+          "IgnoreSlotMethods": true,
+          "IgnoreStubs": true,
+          "IgnoreVariadicNames": false
+        },
+        "ValidEncodings": "latin-1, utf-8"
+      }
+    },
     "DESCRIPTION": "Plug-in implementing an 'ollama' client and interface widgets.",
     "DOCSTRING": "ericdoc",
     "DOCUMENTATIONPARMS": {
@@ -43,7 +185,6 @@
       "*.pyw3": "SOURCES",
       "*.qm": "TRANSLATIONS",
       "*.rst": "OTHERS",
-      "*.svg": "OTHERS",
       "*.toml": "OTHERS",
       "*.ts": "TRANSLATIONS",
       "*.txt": "OTHERS",
@@ -56,7 +197,10 @@
       "README.*": "OTHERS",
       "makefile": "OTHERS"
     },
-    "FORMS": [],
+    "FORMS": [
+      "OllamaInterface/OllamaHistoryWidget.ui",
+      "OllamaInterface/OllamaWidget.ui"
+    ],
     "HASH": "92d9e369bad01266911c1d6eefedae578e76ceb4",
     "IDLPARAMS": {
       "DefinedNames": [],
@@ -135,7 +279,7 @@
     },
     "PACKAGERSPARMS": {},
     "PROGLANGUAGE": "Python3",
-    "PROJECTTYPE": "PyQt6",
+    "PROJECTTYPE": "E7Plugin",
     "PROJECTTYPESPECIFICDATA": {},
     "PROTOCOLS": [],
     "RCCPARAMS": {
@@ -147,6 +291,10 @@
     "RESOURCES": [],
     "SOURCES": [
       "OllamaInterface/OllamaClient.py",
+      "OllamaInterface/OllamaHistoryWidget.py",
+      "OllamaInterface/OllamaWidget.py",
+      "OllamaInterface/Ui_OllamaHistoryWidget.py",
+      "OllamaInterface/Ui_OllamaWidget.py",
       "OllamaInterface/__init__.py",
       "PluginAiOllama.py",
       "__init__.py"
--- a/PluginAiOllama.py	Sun Aug 04 16:57:01 2024 +0200
+++ b/PluginAiOllama.py	Mon Aug 05 18:37:16 2024 +0200
@@ -9,11 +9,24 @@
 
 import os
 
-from PyQt6.QtCore import QObject, Qt, QTranslator
+from PyQt6.QtCore import QObject, Qt, QTranslator, pyqtSignal
+from PyQt6.QtGui import QKeySequence
 
 from eric7 import Preferences
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricAction import EricAction
 from eric7.EricWidgets.EricApplication import ericApp
 
+try:
+    from eric7.UI.UserInterface import UserInterfaceSide
+
+    _Side = UserInterfaceSide.Right
+except ImportError:
+    # backward compatibility for eric < 24.2
+    from eric7.UI.UserInterface import UserInterface
+
+    _Side = UserInterface.RightSide
+
 # Start-Of-Header
 __header__ = {
     "name": "ollama Interface",
@@ -81,17 +94,24 @@
     """
     Function to clear the private data of the plug-in.
     """
-    # TODO: not implemented yet
-    pass
+    if ollamaInterfacePluginObject is not None:
+        widget = ollamaInterfacePluginObject.getWidget()
+        if widget is not None:
+            widget.clearHistory()
 
 
 class PluginOllamaInterface(QObject):
     """
     Class implementing the ollama Interface plug-in.
+
+    @signal preferencesChanged() emitted to signal a change of preferences. This
+        signal is simply relayed from the main UI.
     """
 
     PreferencesKey = "Ollama"
 
+    preferencesChanged = pyqtSignal()
+
     def __init__(self, ui):
         """
         Constructor
@@ -107,6 +127,7 @@
             "OllamaScheme": "http",
             "OllamaHost": "localhost",
             "OllamaPort": 11434,
+            "OllamaHeartbeatInterval": 5,  # 5 seconds heartbeat time; 0 = disabled
         }
 
         self.__translator = None
@@ -125,9 +146,52 @@
         @return tuple of None and activation status
         @rtype bool
         """
-        global error
+        from OllamaInterface.OllamaWidget import OllamaWidget
+
+        global error, ollamaInterfacePluginObject
         error = ""  # clear previous error
-        # TODO: not implemented yet
+        ollamaInterfacePluginObject = self
+
+        usesDarkPalette = ericApp().usesDarkPalette()
+        iconSuffix = "dark" if usesDarkPalette else "light"
+
+        self.__widget = OllamaWidget(self, fromEric=True)
+        iconName = (
+            "sbOllama96"
+            if self.__ui.getLayoutType() == "Sidebars"
+            else "ollama22-{0}".format(iconSuffix)
+        )
+        self.__ui.addSideWidget(
+            _Side,
+            self.__widget,
+            EricPixmapCache.getIcon(os.path.join("OllamaInterface", "icons", iconName)),
+            self.tr("ollama AI Interface"),
+        )
+
+        self.__activateAct = EricAction(
+            self.tr("ollama AI Interface"),
+            self.tr("ollama AI Interface"),
+            QKeySequence(self.tr("Ctrl+Alt+Shift+O")),
+            0,
+            self,
+            "ollama_interface_activate",
+        )
+        self.__activateAct.setStatusTip(
+            self.tr("Switch the input focus to the ollama AI window.")
+        )
+        self.__activateAct.setWhatsThis(
+            self.tr(
+                """<b>Activate ollama AI Interface</b>"""
+                """<p>This switches the input focus to the ollama AI window.</p>"""
+            )
+        )
+        self.__activateAct.triggered.connect(self.__activateWidget)
+
+        self.__ui.addEricActions([self.__activateAct], "ui")
+        menu = self.__ui.getMenu("subwindow")
+        menu.addAction(self.__activateAct)
+
+        self.__ui.preferencesChanged.connect(self.preferencesChanged)
 
         return None, True
 
@@ -135,8 +199,14 @@
         """
         Public method to deactivate this plug-in.
         """
-        # TODO: not implemented yet
-        pass
+        self.__ui.preferencesChanged.disconnect(self.preferencesChanged)
+
+        menu = self.__ui.getMenu("subwindow")
+        menu.removeAction(self.__activateAct)
+        self.__ui.removeEricActions([self.__activateAct], "ui")
+        self.__ui.removeSideWidget(self.__widget)
+
+        self.__initialize()
 
     def __loadTranslator(self):
         """
@@ -161,6 +231,15 @@
                     )
                     print("Using default.")
 
+    def getWidget(self):
+        """
+        Public method to get a reference to the 'ollama' widget.
+
+        @return reference to the 'ollama' widget
+        @rtype OllamaWidget
+        """
+        return self.__widget
+
     def __activateWidget(self):
         """
         Private slot to handle the activation of the pipx interface.
@@ -188,7 +267,7 @@
         @return the requested setting value
         @rtype Any
         """
-        if key in ("OllamaPort",):
+        if key in ("OllamaPort", "OllamaHeartbeatInterval"):
             return int(
                 Preferences.Prefs.settings.value(
                     self.PreferencesKey + "/" + key, self.__defaults[key]
@@ -234,16 +313,5 @@
         sidebar.setCurrentWidget(widget)
 
 
-def installDependencies(pipInstall):
-    """
-    Function to install dependencies of this plug-in.
-
-    @param pipInstall function to be called with a list of package names.
-    @type function
-    """
-    # TODO: not implemented yet
-    pass
-
-
 #
 # eflag: noqa = M801, U200

eric ide

mercurial