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/configuration/Appconfig.py b/src/configuration/Appconfig.py
index 108863b84..b7539bc8c 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 4890f0466..0c36a844b 100644
--- a/src/frontEnd/Application.py
+++ b/src/frontEnd/Application.py
@@ -30,8 +30,8 @@
current_dir = os.path.dirname(os.path.abspath(__file__))
init_path = os.path.abspath(os.path.join(current_dir, "..", "..")) + os.sep
-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
@@ -42,7 +42,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.
@@ -272,8 +272,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(
@@ -359,7 +359,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):
@@ -383,11 +383,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()
@@ -417,7 +417,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):
@@ -532,7 +532,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))
@@ -568,7 +568,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(
@@ -587,7 +587,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):
"""
@@ -628,7 +628,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):
"""
@@ -684,7 +684,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)
@@ -693,7 +693,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):
"""
@@ -726,11 +726,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")
@@ -783,7 +783,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)
@@ -817,7 +817,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)
@@ -842,7 +842,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 860aa0f2a..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
@@ -406,7 +407,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():
@@ -428,7 +429,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
@@ -436,7 +437,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
@@ -564,8 +565,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)
outer.setObjectName("card")
@@ -582,7 +583,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(
@@ -591,11 +592,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)
@@ -656,7 +657,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(
@@ -693,13 +694,13 @@ def __init__(self, session_id: str, title: str, date: str,
meta_row.setContentsMargins(0, 0, 0, 0)
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;
@@ -762,7 +763,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)
@@ -877,7 +878,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)
@@ -902,7 +903,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;
@@ -957,7 +958,7 @@ def _apply_filter(self):
preview = next((m[5:].strip() for m in msgs if m.startswith("User:")), "")
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)
@@ -1075,7 +1076,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)
@@ -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("↻")
@@ -1205,7 +1205,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)
@@ -1264,7 +1264,8 @@ def __init__(self):
self._temp_label = QLabel(f"Precision {self._temperature:.2f}")
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)
@@ -1276,7 +1277,8 @@ def __init__(self):
self._tok_label = QLabel(f"Max tokens {self._num_predict}")
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)
@@ -1408,8 +1410,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; }")
self._thumb_container = QWidget()
@@ -1423,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 ─────────────────────────────────────────────
@@ -1475,7 +1574,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)
@@ -1500,7 +1599,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
@@ -1561,7 +1660,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:
if os.path.exists(_SESSIONS_DIR):
@@ -1585,7 +1684,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:
@@ -1659,6 +1758,8 @@ def _rebuild_chat_html_from_history(self):
def _on_session_clicked(self, item):
session_id = item.data(Qt.UserRole)
+
+ # If this is the session already showing, do nothing.
if (session_id == self._current_session_id
and not self._viewing_past_session):
return
@@ -1939,7 +2040,9 @@ 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))
self._scroll_to_bottom()
self._typing_anim_timer.start(400)
@@ -1950,7 +2053,10 @@ def _animate_typing_bubble(self):
if anchor_cursor is None:
self._typing_anim_timer.stop()
return
- anchor_cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
+ # 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.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:
@@ -1960,7 +2066,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()
self._typing_start_pos = -1
@@ -2056,7 +2162,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():
@@ -2073,7 +2179,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)
@@ -2419,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:
@@ -2742,7 +2851,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:
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