185 @param model name of the model |
175 @param model name of the model |
186 @type str |
176 @type str |
187 @param jsonStr string containing JSON serialize chat history data (defaults |
177 @param jsonStr string containing JSON serialize chat history data (defaults |
188 to None) |
178 to None) |
189 @type str (optional) |
179 @type str (optional) |
|
180 @return reference to the created history widget |
|
181 @rtype OllamaHistoryWidget |
190 """ |
182 """ |
191 history = OllamaHistoryWidget(title=title, model=model, jsonStr=jsonStr) |
183 history = OllamaHistoryWidget(title=title, model=model, jsonStr=jsonStr) |
192 self.__chatHistoryLayout.insertWidget( |
184 self.__chatHistoryLayout.insertWidget( |
193 self.__chatHistoryLayout.count() - 1, history |
185 self.__chatHistoryLayout.count() - 1, history |
194 ) |
186 ) |
195 |
187 |
|
188 history.deleteChatHistory.connect(self.__deleteHistory) |
|
189 history.dataChanged.connect(self.__saveHistory) |
|
190 history.newChatWithHistory.connect(self.__newChatWithHistory) |
|
191 |
|
192 self.__saveHistory() |
|
193 |
|
194 QTimer.singleShot(0, self.__scrollHistoryToBottom) |
|
195 |
|
196 return history |
|
197 |
|
198 @pyqtSlot() |
|
199 def __scrollHistoryToBottom(self): |
|
200 """ |
|
201 Private slot to scroll the history widget to the bottom. |
|
202 """ |
196 scrollbar = self.historyScrollArea.verticalScrollBar() |
203 scrollbar = self.historyScrollArea.verticalScrollBar() |
197 scrollbar.setMaximum(self.historyScrollWidget.height()) |
204 scrollbar.setMaximum(self.historyScrollWidget.height()) |
198 scrollbar.setValue(scrollbar.maximum()) |
205 scrollbar.setValue(scrollbar.maximum()) |
199 |
206 |
200 history.deleteChatHistory.connect(self.__deleteHistory) |
207 def __findHistoryWidget(self, hid): |
201 history.dataChanged.connect(self.__saveHistory) |
208 """ |
202 history.newChatWithHistory.connect(self.__newChatWithHistory) |
209 Private method to find the widget of a given chat history ID. |
203 |
210 |
204 self.__saveHistory() |
211 @param hid ID of the chat history |
205 |
212 @type str |
206 def __findHistoryWidgetIndex(self, uid): |
213 @return reference to the chat history widget |
207 """ |
214 @rtype OllamaHistoryWidget |
208 Private method to find the index of the reference history widget. |
|
209 |
|
210 @param uid ID of the history widget |
|
211 @type str |
|
212 @return index of the history widget |
|
213 @rtype int |
|
214 """ |
215 """ |
215 for index in range(self.__chatHistoryLayout.count() - 1): |
216 for index in range(self.__chatHistoryLayout.count() - 1): |
216 widget = self.__chatHistoryLayout.itemAt(index).widget() |
217 widget = self.__chatHistoryLayout.itemAt(index).widget() |
217 if widget.getId() == uid: |
218 if widget.getId() == hid: |
218 return index |
219 return widget |
219 |
220 |
220 return None |
221 return None |
221 |
222 |
222 def __historyFilePath(self): |
223 def __historyFilePath(self): |
223 """ |
224 """ |
293 item.widget().deleteLater() |
294 item.widget().deleteLater() |
294 |
295 |
295 self.__saveHistory() |
296 self.__saveHistory() |
296 |
297 |
297 @pyqtSlot(str) |
298 @pyqtSlot(str) |
298 def __deleteHistory(self, uid): |
299 def __deleteHistory(self, hid): |
299 """ |
300 """ |
300 Private slot to delete the history with the given ID. |
301 Private slot to delete the history with the given ID. |
301 |
302 |
302 @param uid ID of the history to be deleted |
303 @param hid ID of the history to be deleted |
303 @type str |
304 @type str |
304 """ |
305 """ |
305 widgetIndex = self.__findHistoryWidgetIndex(uid) |
306 widget = self.__findHistoryWidget(hid) |
306 if widgetIndex is not None: |
307 if widget is not None: |
|
308 widgetIndex = self.__chatHistoryLayout.indexOf(widget) |
307 item = self.__chatHistoryLayout.takeAt(widgetIndex) |
309 item = self.__chatHistoryLayout.takeAt(widgetIndex) |
308 if item is not None: |
310 if item is not None: |
309 item.widget().deleteLater() |
311 item.widget().deleteLater() |
310 |
312 |
311 self.__saveHistory() |
313 self.__saveHistory() |
|
314 |
|
315 self.__removeChatWidget(hid) |
|
316 |
|
317 ####################################################################### |
|
318 ## Chat related methods below |
|
319 ####################################################################### |
|
320 |
|
321 def __findChatWidget(self, hid): |
|
322 """ |
|
323 Private method to find a chat widget given a chat history ID. |
|
324 |
|
325 @param hid chat history ID |
|
326 @type str |
|
327 @return reference to the chat widget related to the given ID |
|
328 @rtype OllamaChatWidget |
|
329 """ |
|
330 for index in range(self.chatStackWidget.count()): |
|
331 widget = self.chatStackWidget.widget(index) |
|
332 if widget.getHistoryId() == hid: |
|
333 return widget |
|
334 |
|
335 return None |
|
336 |
|
337 @pyqtSlot() |
|
338 def on_newChatButton_clicked(self): |
|
339 """ |
|
340 Private slot to start a new chat with the 'ollama' server. |
|
341 """ |
|
342 model = self.modelComboBox.currentText() |
|
343 if not model: |
|
344 EricMessageBox.critical( |
|
345 self, |
|
346 self.tr("New Chat"), |
|
347 self.tr("""A model has to be selected first. Aborting..."""), |
|
348 ) |
|
349 return |
|
350 |
|
351 title, ok = QInputDialog.getText( |
|
352 self, |
|
353 self.tr("New Chat"), |
|
354 self.tr("Enter a title for the new chat:"), |
|
355 QLineEdit.EchoMode.Normal, |
|
356 ) |
|
357 if ok and title: |
|
358 historyWidget = self.__createHistoryWidget(title, model) |
|
359 hid = historyWidget.getId() |
|
360 chatWidget = OllamaChatWidget(hid=hid, title=title, model=model) |
|
361 index = self.chatStackWidget.addWidget(chatWidget) |
|
362 self.chatStackWidget.setCurrentIndex(index) |
|
363 |
|
364 self.__updateMessageEditState() |
|
365 self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
312 |
366 |
313 @pyqtSlot(str) |
367 @pyqtSlot(str) |
314 def __newChatWithHistory(self, uid): |
368 def __newChatWithHistory(self, hid): |
315 """ |
369 """ |
316 Private slot to start a new chat using a previously saved history. |
370 Private slot to start a new chat using a previously saved history. |
317 |
371 |
318 @param uid ID of the history to be used |
372 @param hid ID of the history to be used |
319 @type str |
373 @type str |
320 """ |
374 """ |
321 # TODO: not implemented yet |
375 chatWidget = self.__findChatWidget(hid) |
322 pass |
376 if chatWidget is None: |
|
377 historyWidget = self.__findHistoryWidget(hid) |
|
378 if historyWidget is None: |
|
379 # Oops, treat it as a new chat. |
|
380 self.on_newChatButton_clicked() |
|
381 return |
|
382 |
|
383 chatWidget = OllamaChatWidget( |
|
384 hid=hid, title=historyWidget.getTitle(), model=historyWidget.getModel() |
|
385 ) |
|
386 index = self.chatStackWidget.addWidget(chatWidget) |
|
387 self.chatStackWidget.setCurrentIndex(index) |
|
388 for message in historyWidget.getMessages(): |
|
389 chatWidget.addMessage(role=message["role"], message=message["content"]) |
|
390 else: |
|
391 # simply switch to the already existing chatWidget |
|
392 self.chatStackWidget.setCurrentWidget(chatWidget) |
|
393 |
|
394 self.__updateMessageEditState() |
|
395 self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
396 |
|
397 def __removeChatWidget(self, hid): |
|
398 """ |
|
399 Private method to remove a chat widget given its chat history ID. |
|
400 |
|
401 @param hid chat history ID |
|
402 @type str |
|
403 """ |
|
404 widget = self.__findChatWidget(hid) |
|
405 if widget is not None: |
|
406 self.chatStackWidget.removeWidget(widget) |
|
407 |
|
408 @pyqtSlot() |
|
409 def __updateMessageEditState(self): |
|
410 """ |
|
411 Private slot to set the enabled state of the message line edit and the send |
|
412 button. |
|
413 """ |
|
414 chatActive = bool(self.chatStackWidget.count()) |
|
415 hasText = bool(self.messageEdit.text()) |
|
416 |
|
417 self.messageEdit.setEnabled(chatActive) |
|
418 self.sendButton.setEnabled(chatActive and hasText) |
|
419 |
|
420 @pyqtSlot(str) |
|
421 def on_messageEdit_textChanged(self, msg): |
|
422 """ |
|
423 Private slot to handle a change of the entered message. |
|
424 |
|
425 @param msg text of the message line edit |
|
426 @type str |
|
427 """ |
|
428 self.sendButton.setEnabled(bool(msg)) |
|
429 |
|
430 @pyqtSlot() |
|
431 def __sendMessage(self): |
|
432 """ |
|
433 Private method to send the given message of the current chat to the |
|
434 'ollama' server. |
|
435 |
|
436 This sends the message with context (i.e. the history of the current chat). |
|
437 """ |
|
438 msg = self.messageEdit.text() |
|
439 if not msg: |
|
440 # empty message => ignore |
|
441 return |
|
442 |
|
443 if not bool(self.chatStackWidget.count()): |
|
444 # no current stack => ignore |
|
445 return |
|
446 |
|
447 # 1. determine hid of the current chat via chat stack widget |
|
448 chatWidget = self.chatStackWidget.currentWidget() |
|
449 hid = chatWidget.getHistoryId() |
|
450 |
|
451 # 2. get chat history widget via hid from chat history widget |
|
452 historyWidget = self.__findHistoryWidget(hid) |
|
453 if historyWidget is not None: |
|
454 # 3. append the message to the history |
|
455 historyWidget.addToMessages("user", msg) |
|
456 |
|
457 # 4. get the complete messages list from the history |
|
458 messages = historyWidget.getMessages() |
|
459 |
|
460 # 5. add the message to the current chat and an empty one |
|
461 # for the response |
|
462 chatWidget.addMessage("user", msg) |
|
463 chatWidget.addMessage("assistant", "") |
|
464 |
|
465 # 6. send the request via the client (non-streaming (?)) |
|
466 model = historyWidget.getModel() |
|
467 self.__client.chat( |
|
468 model=model, |
|
469 messages=messages, |
|
470 streaming=self.__plugin.getPreferences("StreamingChatResponse"), |
|
471 ) |
|
472 |
|
473 # 7. clear the message editor and give input focus back |
|
474 self.messageEdit.clear() |
|
475 self.messageEdit.setFocus(Qt.FocusReason.OtherFocusReason) |
|
476 |
|
477 @pyqtSlot(str, str, bool) |
|
478 def __handleServerMessage(self, content, role, done): |
|
479 """ |
|
480 Private slot handling an 'ollama' server chat response. |
|
481 |
|
482 @param content message sent by the server |
|
483 @type str |
|
484 @param role role name |
|
485 @type str |
|
486 @param done flag indicating the last chat response |
|
487 @type bool |
|
488 """ |
|
489 if not bool(self.chatStackWidget.count()): |
|
490 # no current stack => ignore |
|
491 return |
|
492 |
|
493 chatWidget = self.chatStackWidget.currentWidget() |
|
494 chatWidget.appendMessage(content) |
|
495 if done: |
|
496 hid = chatWidget.getHistoryId() |
|
497 historyWidget = self.__findHistoryWidget(hid) |
|
498 if historyWidget is not None: |
|
499 historyWidget.addToMessages(role, chatWidget.getRecentMessage()) |
323 |
500 |
324 ####################################################################### |
501 ####################################################################### |
325 ## Menu related methods below |
502 ## Menu related methods below |
326 ####################################################################### |
503 ####################################################################### |
327 |
504 |