From be4a74355aced4d70662e079eadbaaf4c71a173a Mon Sep 17 00:00:00 2001 From: apurvafx Date: Tue, 16 Jun 2026 11:41:34 +0530 Subject: [PATCH 1/3] fix: migrate frontEnd enums to PyQt6 and add standalone chatbot launcher for chatbot testing --- src/configuration/Appconfig.py | 2 +- src/frontEnd/Application.py | 42 ++++++++++++------------ src/frontEnd/Chatbot.py | 58 ++++++++++++++++----------------- src/frontEnd/DockArea.py | 19 ++++++----- src/frontEnd/ProjectExplorer.py | 24 +++++++------- src/frontEnd/Workspace.py | 6 ++-- src/run_chatbot.py | 15 +++++++++ 7 files changed, 91 insertions(+), 75 deletions(-) create mode 100644 src/run_chatbot.py diff --git a/src/configuration/Appconfig.py b/src/configuration/Appconfig.py index a2f18ab61..a0f36d6b5 100644 --- a/src/configuration/Appconfig.py +++ b/src/configuration/Appconfig.py @@ -17,7 +17,7 @@ # REVISION: Thursday 29 June 2023 # ========================================================================= -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets import os import json from configparser import ConfigParser diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 14cf662bd..1d5cbbb8c 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -29,8 +29,8 @@ import pathmagic # noqa:F401 init_path = '../../' -from PyQt5 import QtGui, QtCore, QtWidgets -from PyQt5.Qt import QSize +from PyQt6 import QtGui, QtCore, QtWidgets + from configuration.Appconfig import Appconfig from frontEnd import ProjectExplorer from frontEnd import Workspace @@ -41,7 +41,7 @@ from projManagement.Validation import Validation from projManagement import Worker from frontEnd.Chatbot import ChatbotGUI -from PyQt5.QtCore import QTimer +from PyQt6.QtCore import QTimer, Qsize # Its our main window of application. @@ -271,8 +271,8 @@ def initToolBar(self): # corner in the application window. self.spacer = QtWidgets.QWidget() self.spacer.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) self.topToolbar.addWidget(self.spacer) self.logo = QtWidgets.QLabel() self.logopic = QtGui.QPixmap( @@ -358,7 +358,7 @@ def initToolBar(self): self.lefttoolbar.addAction(self.omedit) self.lefttoolbar.addAction(self.omoptim) self.lefttoolbar.addAction(self.conToeSim) - self.lefttoolbar.setOrientation(QtCore.Qt.Vertical) + self.lefttoolbar.setOrientation(QtCore.Qt.Orientation.Vertical) self.lefttoolbar.setIconSize(QSize(40, 40)) def closeEvent(self, event): @@ -382,11 +382,11 @@ def closeEvent(self, event): exit_msg = "Are you sure you want to exit the program?" exit_msg += " All unsaved data will be lost." reply = QtWidgets.QMessageBox.question( - self, 'Message', exit_msg, QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No + self, 'Message', exit_msg, QtWidgets.QMessageBox.StandardButton.Yes, + QtWidgets.QMessageBox.StandardButton.No ) - if reply == QtWidgets.QMessageBox.Yes: + if reply == QtWidgets.QMessageBox.StandardButton.Yes: for proc in self.obj_appconfig.procThread_list: try: proc.terminate() @@ -412,7 +412,7 @@ def closeEvent(self, event): event.accept() self.systemTrayIcon.showMessage('Exit', 'eSim is Closed.') - elif reply == QtWidgets.QMessageBox.No: + elif reply == QtWidgets.QMessageBox.StandardButton.No: event.ignore() def new_project(self): @@ -527,7 +527,7 @@ def plotSimulationData(self, exitCode, exitStatus): self.msg.showMessage( 'Data could not be plotted. Please try again.' ) - self.msg.exec_() + self.msg.exec() print("Exception Message:", str(e), traceback.format_exc()) self.obj_appconfig.print_error('Exception Message : ' + str(e)) @@ -563,7 +563,7 @@ def open_ngspice(self): self.msg.showMessage( 'Netlist (*.cir.out) not found.' ) - self.msg.exec_() + self.msg.exec() return self.obj_Mainview.obj_dockarea.ngspiceEditor( @@ -582,7 +582,7 @@ def open_ngspice(self): 'Please select the project first.' ' You can either create new project or open existing project' ) - self.msg.exec_() + self.msg.exec() def open_subcircuit(self): """ @@ -623,7 +623,7 @@ def open_nghdl(self): 'Please make sure it is installed') self.obj_appconfig.print_error('Error while opening NGHDL. ' + 'Please make sure it is installed') - self.msg.exec_() + self.msg.exec() def open_makerchip(self): """ @@ -679,7 +679,7 @@ def open_OMedit(self): 'Current project does not contain any Ngspice file. ' + 'Please create Ngspice file with extension .cir.out' ) - self.msg.exec_() + self.msg.exec() else: self.msg = QtWidgets.QErrorMessage() self.msg.setModal(True) @@ -688,7 +688,7 @@ def open_OMedit(self): 'Please select the project first. You can either ' + 'create a new project or open an existing project' ) - self.msg.exec_() + self.msg.exec() def open_OMoptim(self): """ @@ -721,11 +721,11 @@ def open_OMoptim(self): "https://www.openmodelica.org/download/download-windows" ">OpenModelica Windows and install latest version.
" ) - self.msg.setTextFormat(QtCore.Qt.RichText) + self.msg.setTextFormat(QtCore.Qt.TextFormat.RichText) self.msg.setText(self.msgContent) self.msg.setWindowTitle("Error Message") self.obj_appconfig.print_info(self.msgContent) - self.msg.exec_() + self.msg.exec() def open_conToeSim(self): print("Function : Schematics converter") @@ -778,7 +778,7 @@ def __init__(self, *args): self.obj_projectExplorer = ProjectExplorer.ProjectExplorer() # Adding content to vertical middle Split. - self.middleSplit.setOrientation(QtCore.Qt.Vertical) + self.middleSplit.setOrientation(QtCore.Qt.Orientation.Vertical) self.middleSplit.addWidget(self.obj_dockarea) self.middleSplit.addWidget(self.noteArea) @@ -812,7 +812,7 @@ def main(args): splash_pix = QtGui.QPixmap(init_path + 'images/splash_screen_esim.png') splash = QtWidgets.QSplashScreen( - appView, splash_pix, QtCore.Qt.WindowStaysOnTopHint + appView, splash_pix, QtCore.Qt.WindowType.WindowStaysOnTopHint ) splash.setMask(splash_pix.mask()) splash.setDisabled(True) @@ -837,7 +837,7 @@ def main(args): else: appView.obj_workspace.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) # Call main function diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index decc908d0..6557bd8fb 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -430,7 +430,7 @@ def add_to_history(self, text): def keyPressEvent(self, event: QKeyEvent): # ── Ctrl+V: check for clipboard image before default paste ──── - if event.key() == Qt.Key_V and event.modifiers() & Qt.ControlModifier: + if event.key() == Qt.Key.Key_V and event.modifiers() & Qt.KeyboardModifier.ControlModifier: clipboard = QApplication.clipboard() mime = clipboard.mimeData() if mime and mime.hasImage(): @@ -452,7 +452,7 @@ def keyPressEvent(self, event: QKeyEvent): super().keyPressEvent(event) return - if event.key() == Qt.Key_Up and self._sent_history: + if event.key() == Qt.Key.Key_Up and self._sent_history: if self._hist_idx == -1: self._draft = self.text() self._hist_idx = len(self._sent_history) - 1 @@ -460,7 +460,7 @@ def keyPressEvent(self, event: QKeyEvent): self._hist_idx -= 1 self.setText(self._sent_history[self._hist_idx]) self.end(False) - elif event.key() == Qt.Key_Down and self._hist_idx >= 0: + elif event.key() == Qt.Key.Key_Down and self._hist_idx >= 0: self._hist_idx += 1 if self._hist_idx >= len(self._sent_history): self._hist_idx = -1 @@ -589,8 +589,8 @@ def __init__(self, session: dict, parent=None): class _DeleteConfirmDialog(QDialog): def __init__(self, title: str, parent=None): - super().__init__(parent, Qt.FramelessWindowHint | Qt.Dialog) - self.setAttribute(Qt.WA_TranslucentBackground) + super().__init__(parent, Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setMinimumWidth(320) outer = QWidget(self) @@ -609,7 +609,7 @@ def __init__(self, title: str, parent=None): title_lbl = QLabel("Delete chat?") title_lbl.setStyleSheet("font-size:16px; font-weight:bold; color:#1a1a2e;") - title_lbl.setAlignment(Qt.AlignCenter) + title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) card_layout.addWidget(title_lbl) body_lbl = QLabel( @@ -618,11 +618,11 @@ def __init__(self, title: str, parent=None): f'This cannot be undone.' ) body_lbl.setWordWrap(True) - body_lbl.setAlignment(Qt.AlignCenter) + body_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) card_layout.addWidget(body_lbl) div = QFrame() - div.setFrameShape(QFrame.HLine) + div.setFrameShape(QFrame.Shape.HLine) div.setStyleSheet("color:#f0f0f0;") card_layout.addWidget(div) @@ -684,7 +684,7 @@ def __init__(self, session_id: str, title: str, date: str, avatar = QLabel(title[0].upper() if title else "C") avatar.setFixedSize(38, 38) - avatar.setAlignment(Qt.AlignCenter) + avatar.setAlignment(Qt.AlignmentFlag.AlignCenter) avatar.setStyleSheet(""" QLabel { background: qlineargradient( @@ -724,14 +724,14 @@ def __init__(self, session_id: str, title: str, date: str, kind_lbl = QLabel() kind_lbl.setText(_session_kind_badge(kind)) - kind_lbl.setTextFormat(Qt.RichText) + kind_lbl.setTextFormat(Qt.TextFormat.RichText) kind_lbl.setStyleSheet("background:transparent;") meta_row.addWidget(kind_lbl) if msg_count > 0: count_lbl = QLabel(str(msg_count)) count_lbl.setFixedSize(20, 16) - count_lbl.setAlignment(Qt.AlignCenter) + count_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) count_lbl.setStyleSheet(""" QLabel { background:#0095f6; color:white; @@ -795,7 +795,7 @@ def sizeHint(self): def _on_delete_clicked(self): dlg = _DeleteConfirmDialog(self.title, self) - if dlg.exec() == QDialog.Accepted: + if dlg.exec() == QDialog.DialogCode.Accepted: self.delete_requested.emit(self.session_id) @@ -912,7 +912,7 @@ def __init__(self, parent=None): root.addWidget(controls) sep = QFrame() - sep.setFrameShape(QFrame.HLine) + sep.setFrameShape(QFrame.Shape.HLine) sep.setFixedHeight(1) sep.setStyleSheet("QFrame { background:#f0f0f0; border:none; }") root.addWidget(sep) @@ -937,7 +937,7 @@ def __init__(self, parent=None): root.addWidget(self.session_list) self._empty_lbl = QLabel("No saved chats yet.\nStart a conversation!") - self._empty_lbl.setAlignment(Qt.AlignCenter) + self._empty_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) self._empty_lbl.setStyleSheet(""" QLabel { color:#ccc; font-size:12px; @@ -1000,7 +1000,7 @@ def _apply_filter(self): kind = s.get('kind', 'text') item = QListWidgetItem() - item.setData(Qt.UserRole, sid) + item.setData(Qt.ItemDataRole.UserRole, sid) widget = _SessionItemWidget(sid, title, date, msg_count, preview, kind, self.session_list) widget.delete_requested.connect(self._delete_session) widget.rename_requested.connect(self.rename_requested) @@ -1100,7 +1100,7 @@ def __init__(self): border-radius:14px; padding:4px 14px; } """) - self._toast.setAlignment(Qt.AlignCenter) + self._toast.setAlignment(Qt.AlignmentFlag.AlignCenter) self._toast.hide() root = QHBoxLayout(self) @@ -1231,7 +1231,7 @@ def __init__(self): self._update_ollama_status() header_sep = QFrame() - header_sep.setFrameShape(QFrame.HLine) + header_sep.setFrameShape(QFrame.Shape.HLine) header_sep.setStyleSheet("color:#ececec; margin:0;") chat_layout.addLayout(header_layout) chat_layout.addWidget(header_sep) @@ -1293,7 +1293,7 @@ def __init__(self): self._temp_label.setStyleSheet("font-size:10px; color:#555;") temp_col.addWidget(self._temp_label) - self._temp_slider = QSlider(Qt.Horizontal) + self._temp_slider = QSlider(Qt.Orientation.Horizontal) self._temp_slider.setRange(1, 100) self._temp_slider.setValue(int(self._temperature * 100)) self._temp_slider.setFixedWidth(110) @@ -1306,7 +1306,7 @@ def __init__(self): self._tok_label.setStyleSheet("font-size:10px; color:#555;") tok_col.addWidget(self._tok_label) - self._tok_slider = QSlider(Qt.Horizontal) + self._tok_slider = QSlider(Qt.Orientation.Horizontal) self._tok_slider.setRange(1, 40) self._tok_slider.setValue(self._num_predict // 128) self._tok_slider.setFixedWidth(110) @@ -1443,8 +1443,8 @@ def __init__(self): scroll = QScrollArea() scroll.setFixedHeight(72) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll.setWidgetResizable(True) scroll.setStyleSheet("QScrollArea { border:none; background:transparent; }") @@ -1509,7 +1509,7 @@ def _refresh_sidebar_if_open(self): def _delete_all_chats(self): dlg = _DeleteConfirmDialog("all chats", self) - if dlg.exec() != QDialog.Accepted: + if dlg.exec() != QDialog.DialogCode.Accepted: return try: @@ -1523,7 +1523,7 @@ def _delete_all_chats(self): self._sidebar.populate() def _open_session_viewer(self, item): - session_id = item.data(Qt.UserRole) + session_id = item.data(Qt.ItemDataRole.UserRole) path = os.path.join(_SESSIONS_DIR, f"{session_id}.json") try: with open(path, encoding='utf-8') as f: @@ -1601,7 +1601,7 @@ def _rebuild_chat_html_from_history(self): self._scroll_to_bottom() def _on_session_clicked(self, item): - session_id = item.data(Qt.UserRole) + session_id = item.data(Qt.ItemDataRole.UserRole) # If this is the session already showing, do nothing. if (session_id == self._current_session_id @@ -1990,7 +1990,7 @@ def _find_typing_anchor_cursor(self): def _show_typing_bubble(self): self._typing_frame = 0 cursor = QTextCursor(self.chat_display.document()) - cursor.movePosition(QTextCursor.End) + cursor.movePosition(QTextCursor.MoveOperation.End) # Insert sentinel anchor + bubble in one operation so they form # a contiguous block that can be fully removed later. cursor.insertHtml(self._TYPING_ANCHOR + _typing_bubble(0)) @@ -2007,7 +2007,7 @@ def _animate_typing_bubble(self): # Select from the sentinel to the end of the document and replace. # This is immune to any reflow that happened while the window was # in the background because we locate by anchor name, not position. - anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + anchor_cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) anchor_cursor.insertHtml(self._TYPING_ANCHOR + _typing_bubble(self._typing_frame)) # Only auto-scroll if the user is already near the bottom so we # don't hijack their scroll position while they read earlier msgs. @@ -2019,7 +2019,7 @@ def _remove_typing_bubble(self): self._typing_anim_timer.stop() anchor_cursor = self._find_typing_anchor_cursor() if anchor_cursor is not None: - anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + anchor_cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) anchor_cursor.removeSelectedText() # Legacy guard: if somehow _typing_start_pos path left stale state self._typing_start_pos = -1 @@ -2120,7 +2120,7 @@ def _make_thumbnail(self, image_path: str) -> QWidget: card_layout.setSpacing(2) thumb_lbl = QLabel() - thumb_lbl.setAlignment(Qt.AlignCenter) + thumb_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) thumb_lbl.setFixedHeight(36) pix = QPixmap(image_path) if not pix.isNull(): @@ -2137,7 +2137,7 @@ def _make_thumbnail(self, image_path: str) -> QWidget: fname = os.path.basename(image_path) name_lbl = QLabel(fname[:10] + ("…" if len(fname) > 10 else "")) - name_lbl.setAlignment(Qt.AlignCenter) + name_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) name_lbl.setStyleSheet("font-size:9px;color:#555;background:transparent;") card_layout.addWidget(name_lbl) diff --git a/src/frontEnd/DockArea.py b/src/frontEnd/DockArea.py index a63c87379..9a0fe6b40 100755 --- a/src/frontEnd/DockArea.py +++ b/src/frontEnd/DockArea.py @@ -1,4 +1,4 @@ -from PyQt5 import QtCore, QtWidgets +from PyQt6 import QtCore, QtWidgets from ngspiceSimulation import plotWindow from ngspiceSimulation.NgspiceWidget import NgspiceWidget from configuration.Appconfig import Appconfig @@ -9,8 +9,9 @@ from browser.Welcome import Welcome from browser.UserManual import UserManual from ngspicetoModelica.ModelicaUI import OpenModelicaEditor -from PyQt5.QtWidgets import QLineEdit, QLabel, QPushButton, QVBoxLayout, QHBoxLayout -from PyQt5.QtCore import Qt +from PyQt6.QtWidgets import ( + QLineEdit, QLabel, QPushButton, QVBoxLayout, QHBoxLayout) +from PyQt6.QtCore import Qt import os from converter.pspiceToKicad import PspiceConverter from converter.ltspiceToKicad import LTspiceConverter @@ -220,7 +221,7 @@ def eSimConverter(self): file_path_text_box = QLineEdit() file_path_text_box.setFixedHeight(30) file_path_text_box.setFixedWidth(800) - file_path_layout.setAlignment(Qt.AlignCenter) + file_path_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) file_path_layout.addWidget(file_path_text_box) browse_button = QPushButton("Browse") @@ -264,7 +265,7 @@ def eSimConverter(self): # lib_path_text_box = QLineEdit() # lib_path_text_box.setFixedHeight(30) # lib_path_text_box.setFixedWidth(800) - # lib_path_layout.setAlignment(Qt.AlignCenter) + # lib_path_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) # lib_path_layout.addWidget(lib_path_text_box) # browse_button1 = QPushButton("Browse lib") @@ -316,7 +317,7 @@ def eSimConverter(self): self.description_label = QLabel() self.description_label.setFixedHeight(160) self.description_label.setFixedWidth(950) - self.description_label.setAlignment(Qt.AlignBottom) + self.description_label.setAlignment(Qt.AlignmentFlag.AlignBottom) self.description_label.setWordWrap(True) self.description_label.setText(description_html) self.eConLayout.addWidget(self.description_label) # Add the description label to the layout @@ -356,7 +357,7 @@ def modelEditor(self): 'Please select the project first.' ' You can either create new project or open existing project' ) - self.msg.exec_() + self.msg.exec() return projName = os.path.basename(projDir) dockName = f'Model Editor-{projName}-' @@ -483,7 +484,7 @@ def subcircuiteditor(self): 'Please select the project first.' ' You can either create new project or open existing project' ) - self.msg.exec_() + self.msg.exec() def makerchip(self): """This function creates a widget for different subcircuit options.""" @@ -500,7 +501,7 @@ def makerchip(self): 'Please select the project first.' ' You can either create new project or open existing project' ) - self.msg.exec_() + self.msg.exec() return projName = os.path.basename(projDir) dockName = f'Makerchip-{projName}-' diff --git a/src/frontEnd/ProjectExplorer.py b/src/frontEnd/ProjectExplorer.py index bc55dac9c..1056e0bd4 100755 --- a/src/frontEnd/ProjectExplorer.py +++ b/src/frontEnd/ProjectExplorer.py @@ -1,5 +1,5 @@ -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtWidgets import QDockWidget, QMessageBox,QMenu +from PyQt6 import QtCore, QtWidgets +from PyQt6.QtWidgets import QDockWidget, QMessageBox,QMenu import os import json from configuration.Appconfig import Appconfig @@ -70,7 +70,7 @@ def __init__(self): self.window.addWidget(self.treewidget) self.treewidget.expanded.connect(self.refreshInstant) self.treewidget.doubleClicked.connect(self.openProject) - self.treewidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.treewidget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.treewidget.customContextMenuRequested.connect(self.openMenu) self.setLayout(self.window) self.show() @@ -143,7 +143,7 @@ def openMenu(self, position): refresh_action = menu.addAction("Refresh") refresh_action.triggered.connect(self.refreshInstant) - menu.exec_(self.treewidget.viewport().mapToGlobal(position)) + menu.exec(self.treewidget.viewport().mapToGlobal(position)) def openProject(self): self.indexItem = self.treewidget.currentIndex() @@ -273,7 +273,7 @@ def refreshProject(self, filePath=None, indexItem=None): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage('Selected project does not exist.') - msg.exec_() + msg.exec() return False def renameProject(self): @@ -309,7 +309,7 @@ def renameProject(self): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage('The project name cannot be empty') - msg.exec_() + msg.exec() elif self.baseFileName == newBaseFileName: print("Project name has to be different") @@ -318,7 +318,7 @@ def renameProject(self): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage('The project name has to be different') - msg.exec_() + msg.exec() elif self.refreshProject(filePath): @@ -348,7 +348,7 @@ def renameProject(self): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage('Selected project does not exist.') - msg.exec_() + msg.exec() elif reply == "VALID": # rename project folder @@ -367,7 +367,7 @@ def renameProject(self): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage(str(e)) - msg.exec_() + msg.exec() return # rename files matching project name @@ -406,7 +406,7 @@ def renameProject(self): msg.setModal(True) msg.setWindowTitle("Error Message") msg.showMessage(str(e)) - msg.exec_() + msg.exec() return # update project_explorer dictionary @@ -436,7 +436,7 @@ def renameProject(self): '" already exist. Please select a different name or' + ' delete existing project' ) - msg.exec_() + msg.exec() elif reply == "CHECKNAME": print("Name can not contain space between them") @@ -448,7 +448,7 @@ def renameProject(self): 'The project name should not ' + 'contain space between them' ) - msg.exec_() + msg.exec() def _analyze_netlist_in_copilot(self, netlist_path: str): """Send selected .cir file to chatbot for analysis.""" diff --git a/src/frontEnd/Workspace.py b/src/frontEnd/Workspace.py index b6ebdd53a..d41c289ac 100755 --- a/src/frontEnd/Workspace.py +++ b/src/frontEnd/Workspace.py @@ -16,7 +16,7 @@ # REVISION: Sunday 13 December 2020 # ========================================================================= -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from configuration.Appconfig import Appconfig import time import os @@ -45,7 +45,7 @@ def initWorkspace(self): self.mainwindow = QtWidgets.QVBoxLayout() self.split = QtWidgets.QSplitter() - self.split.setOrientation(QtCore.Qt.Vertical) + self.split.setOrientation(QtCore.Qt.Orientation.Vertical) self.grid = QtWidgets.QGridLayout() self.note = QtWidgets.QTextEdit(self) @@ -81,7 +81,7 @@ def initWorkspace(self): self.setGeometry(QtCore.QRect(500, 250, 400, 400)) self.setMaximumSize(4000, 200) self.setWindowTitle("eSim") - self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setWindowFlags(QtCore.Qt.WindowType.WindowStaysOnTopHint) self.setWindowModality(2) init_path = '../../' diff --git a/src/run_chatbot.py b/src/run_chatbot.py new file mode 100644 index 000000000..e3f0cdf2f --- /dev/null +++ b/src/run_chatbot.py @@ -0,0 +1,15 @@ +import sys +sys.path.insert(0, '.') + +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import Qt + +app = QApplication(sys.argv) + +from frontEnd.Chatbot import ChatbotGUI +window = ChatbotGUI() +window.setWindowTitle("eSim AI Chatbot - Test") +window.resize(900, 700) +window.show() + +sys.exit(app.exec()) \ No newline at end of file From 18ef19ca0a3f02481964a14cccc9a2af271df747 Mon Sep 17 00:00:00 2001 From: apurvafx Date: Tue, 16 Jun 2026 12:49:24 +0530 Subject: [PATCH 2/3] fix: fix remaining QTextCursor PyQt5 enums restored by merge conflict --- src/frontEnd/Chatbot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 994d0ad9b..2f2ac7612 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -1477,7 +1477,7 @@ def _begin_streaming_bubble(self): self._stream_ts = _get_time() self._stream_idx = self._response_counter cursor = QTextCursor(self.chat_display.document()) - cursor.movePosition(QTextCursor.End) + cursor.movePosition(QTextCursor.MoveOperation.End) cursor.insertHtml( self._STREAM_ANCHOR + _bot_bubble("…", self._stream_ts, self._stream_idx) @@ -1502,7 +1502,7 @@ def _on_stream_chunk(self, piece: str): return # Select from anchor to end of document and rewrite the bubble in place. - anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + anchor_cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) anchor_cursor.removeSelectedText() anchor_cursor.insertHtml( self._STREAM_ANCHOR @@ -1943,7 +1943,7 @@ def _on_status_result(self, running: bool): def _show_typing_bubble(self): self._typing_frame = 0 cursor = QTextCursor(self.chat_display.document()) - cursor.movePosition(QTextCursor.End) + cursor.movePosition(QTextCursor.MoveOperation.End) # Insert sentinel anchor + bubble in one operation so they form # a contiguous block that can be fully removed later. cursor.insertHtml(self._TYPING_ANCHOR + _typing_bubble(0)) @@ -1959,7 +1959,7 @@ def _animate_typing_bubble(self): # Select from the sentinel to the end of the document and replace. # This is immune to any reflow that happened while the window was # in the background because we locate by anchor name, not position. - anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + anchor_cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) anchor_cursor.insertHtml(self._TYPING_ANCHOR + _typing_bubble(self._typing_frame)) sb = self.chat_display.verticalScrollBar() if sb.maximum() - sb.value() < 60: @@ -2751,7 +2751,7 @@ def display_response(self, bot_response): idx = self._stream_idx anchor_cursor = self._find_stream_anchor_cursor() if anchor_cursor is not None: - anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + anchor_cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) anchor_cursor.removeSelectedText() anchor_cursor.insertHtml(_bot_bubble(bot_response, ts, idx)) else: From ff513ee6a6ee51bbe77f31fa91d818b6ab66f80a Mon Sep 17 00:00:00 2001 From: apurvafx Date: Thu, 18 Jun 2026 16:10:51 +0530 Subject: [PATCH 3/3] feat: automate headless Ollama startup and model pulling --- src/chatbot/chatbot_thread.py | 83 +++++++++++++++++++++--- src/frontEnd/Chatbot.py | 118 +++++++++++++++++++++++++++++++--- 2 files changed, 183 insertions(+), 18 deletions(-) diff --git a/src/chatbot/chatbot_thread.py b/src/chatbot/chatbot_thread.py index 1c8d8f671..101b084ab 100644 --- a/src/chatbot/chatbot_thread.py +++ b/src/chatbot/chatbot_thread.py @@ -162,15 +162,41 @@ def is_ollama_running(): def start_ollama(stop_flag=None): + """Start Ollama server silently in the background (no terminal window).""" + cmd = ["ollama", "serve"] if os.name == 'nt': - subprocess.Popen('start cmd /k "ollama serve"', shell=True) - else: - subprocess.Popen( - ['bash', '-c', - 'x-terminal-emulator -e "ollama serve" || ' - 'gnome-terminal -- ollama serve || ' - 'xterm -e "ollama serve"'] - ) + import shutil + # If 'ollama' is not directly callable from PATH, check default install locations + if not shutil.which("ollama"): + local_appdata = os.environ.get("LOCALAPPDATA", "") + possible_paths = [ + os.path.join(local_appdata, "Programs", "Ollama", "ollama.exe"), + r"C:\Program Files\Ollama\ollama.exe", + r"C:\Program Files (x86)\Ollama\ollama.exe", + ] + for path in possible_paths: + if os.path.exists(path): + cmd = [path, "serve"] + break + try: + if os.name == 'nt': + # Windows: CREATE_NO_WINDOW flag prevents a cmd popup + subprocess.Popen( + cmd, + creationflags=subprocess.CREATE_NO_WINDOW, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + # Linux/macOS: redirect output to /dev/null, no terminal emulator needed + subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + # ollama binary not found in PATH or disk + return False for _ in range(30): if stop_flag is not None and stop_flag(): return False @@ -179,7 +205,6 @@ def start_ollama(stop_flag=None): return True return False - # ── Topic switch detection ─────────────────────────────────────────────────── _STOP_WORDS = { @@ -264,6 +289,46 @@ def run(self): self.result_signal.emit([]) +# ── Model Pull Worker ───────────────────────────────────────────────────────── +# Required models for the chatbot to function correctly +REQUIRED_MODELS = ["qwen2.5-coder:3b", "nomic-embed-text"] +VISION_MODEL = "minicpm-v" # optional — only needed for image analysis + +class ModelPullWorker(QThread): + """ + Downloads a single Ollama model in the background. + Emits: + progress_signal(str) — human-readable status/percentage string + done_signal(bool) — True on success, False on failure + """ + progress_signal = pyqtSignal(str) + done_signal = pyqtSignal(bool) + + def __init__(self, model_name: str): + super().__init__() + self.model_name = model_name + + def run(self): + try: + self.progress_signal.emit(f"⬇️ Downloading {self.model_name}… 0%") + for update in ollama.pull(self.model_name, stream=True): + # update is a dict with keys: status, completed, total + status = update.get("status", "") + completed = update.get("completed", 0) + total = update.get("total", 0) + if total and completed: + pct = int((completed / total) * 100) + self.progress_signal.emit( + f"⬇️ Downloading {self.model_name}… {pct}%" + ) + elif status: + self.progress_signal.emit( + f"⬇️ {self.model_name}: {status}" + ) + self.done_signal.emit(True) + except Exception as e: + self.progress_signal.emit(f"❌ Failed to download {self.model_name}: {e}") + self.done_signal.emit(False) # ── Smart token budget ─────────────────────────────────────────────────────── _COMPLEX_KEYWORDS = { diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 2f2ac7612..54bdaf3e7 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -19,11 +19,12 @@ else: init_path = '../../' -from chatbot.chatbot_thread import ( # type: ignore +from chatbot.chatbot_thread import ( OllamaWorker, OllamaVisionWorker, MicWorker, OllamaStatusWorker, ModelFetchWorker, + ModelPullWorker, REQUIRED_MODELS, VISION_MODEL, detect_topic_switch, get_stt_backend, - VISION_MODEL_KEYWORDS, # EXTRACTED: shared constant, avoids duplicate keyword list + VISION_MODEL_KEYWORDS, ) from PyQt6.QtWidgets import ( QWidget, QHBoxLayout, QTextBrowser, QVBoxLayout, @@ -31,7 +32,7 @@ QFileDialog, QDialog, QListWidget, QListWidgetItem, QFrame, QScrollArea, QSlider, QInputDialog ) -from PyQt6.QtCore import QTimer, Qt, pyqtSignal, QSize +from PyQt6.QtCore import QTimer, Qt, pyqtSignal, QSize, QThread from PyQt6.QtGui import QTextCursor, QKeyEvent, QDragEnterEvent, QDropEvent from configuration.Appconfig import Appconfig from datetime import datetime @@ -1128,7 +1129,6 @@ def __init__(self): QComboBox:focus { border:1px solid #0095f6; background:#fff; } QComboBox::drop-down { border:none; width:18px; } """) - self._populate_models() header_layout.addWidget(self.model_combo) self._refresh_models_btn = QPushButton("↻") @@ -1425,6 +1425,103 @@ def __init__(self): self.move_to_bottom_right() self._load_history() + self._startup_check() + # ── Startup: headless server + auto-pull ───────────────────────────── + + def _startup_check(self): + """ + Called once on startup. + 1. If Ollama is not running, start it headlessly. + 2. Check which required models are missing. + 3. Pull each missing model one-by-one with live progress. + """ + from chatbot.chatbot_thread import is_ollama_running, start_ollama + + self.user_input.setEnabled(False) + self.send_button.setEnabled(False) + self.status_label.setText("🔄 Starting Ollama server…") + + if not is_ollama_running(): + self.status_label.setText("🔄 Starting Ollama in background…") + # start_ollama blocks for up to 30s waiting for the server + # Run it in a thread so the UI does not freeze + self._ollama_start_worker = OllamaStatusWorker() + self._ollama_start_worker.result_signal.connect(self._on_server_ready) + # Reuse OllamaStatusWorker just to trigger a background start + QTimer.singleShot(0, lambda: self._boot_server_then_check()) + else: + self._check_and_pull_models() + + def _boot_server_then_check(self): + """Run start_ollama() in a background thread, then check models.""" + from chatbot.chatbot_thread import start_ollama + + class _BootWorker(QThread): + result_ready = pyqtSignal(bool) + def run(self): + self.result_ready.emit(start_ollama()) + + self._boot_worker = _BootWorker() + self._boot_worker.result_ready.connect(self._on_server_ready) + self._boot_worker.start() + + def _on_server_ready(self, success: bool): + if success: + self.status_label.setText("✅ Ollama server is running.") + self._check_and_pull_models() + else: + self.status_label.setText( + "❌ Could not start Ollama. " + "Please install it from https://ollama.com and restart eSim." + ) + # Leave input disabled + + def _check_and_pull_models(self): + """Check installed models and pull anything that is missing.""" + from chatbot.chatbot_thread import _fetch_model_names + try: + installed = _fetch_model_names() + except Exception: + installed = [] + + installed_lower = [m.lower() for m in installed] + missing = [ + m for m in REQUIRED_MODELS + if not any(m.lower() in i for i in installed_lower) + ] + + if not missing: + self.status_label.setText("✅ All models ready!") + self.user_input.setEnabled(True) + self.send_button.setEnabled(True) + self._update_ollama_status() + return + + # Pull missing models one by one + self._pull_queue = missing + self._pull_next_model() + + def _pull_next_model(self): + if not self._pull_queue: + self.status_label.setText("✅ All models downloaded and ready!") + self.user_input.setEnabled(True) + self.send_button.setEnabled(True) + self._populate_models() + return + + model = self._pull_queue.pop(0) + self.status_label.setText(f"⬇️ Downloading {model}… 0%") + self._pull_worker = ModelPullWorker(model) + self._pull_worker.progress_signal.connect(self.status_label.setText) + self._pull_worker.done_signal.connect(self._on_model_pulled) + self._pull_worker.start() + + def _on_model_pulled(self, success: bool): + if success: + self._pull_next_model() # pull the next one in the queue + else: + # Failed but continue trying the rest + self._pull_next_model() # ── Streaming helpers ───────────────────────────────────────────── @@ -2428,15 +2525,18 @@ def _populate_models(self): def _on_models_fetched(self, model_names: list): self.model_combo.clear() + from chatbot.chatbot_thread import is_ollama_running if not model_names: - # No models found — Ollama may be offline or has no models pulled. self.model_combo.addItem("No models found") self.model_combo.setEnabled(False) - self.status_label.setText( - "⚠️ No Ollama models found. Run 'ollama pull qwen2.5-coder' " - "in a terminal to install one." - ) + if is_ollama_running(): + self.status_label.setText( + "⚠️ No Ollama models found. Run 'ollama pull qwen2.5-coder:3b' " + "in a terminal to install one." + ) + else: + self.status_label.setText("🔴 Ollama is offline.") return for name in model_names: