2323"""
2424
2525import configparser
26- import datetime
27- import locale as py_locale
28- import logging
29- import os
30- import platform
31- import shutil
32- import sqlite3
33- import sys
34- import urllib .error as urllib_err
35- import urllib .request
36- import webbrowser
37- import zipfile
38- from copy import copy
39- from pathlib import Path
40- from typing import Any , Optional
41-
42- from PyQt6 import QtCore , QtGui , QtWidgets
43-
44- from qualcoder .ai_mcp_server import AiMcpServer
45- from qualcoder .ai_llm import get_default_ai_models , update_ai_models
46- from qualcoder .ai_runtime import (
47- AI_DISABLED ,
48- AI_INITIALIZING ,
49- AI_READY ,
50- AI_UNLOADED ,
51- VECTORSTORE_DISABLED ,
52- VECTORSTORE_UNLOADED ,
53- vectorstore_required ,
54- )
55- from qualcoder .helpers import get_default_user_directory , Message
56- from qualcoder .speakers import speaker_coder_name
26+ import datetime
27+ import locale as py_locale
28+ import logging
29+ import os
30+ import platform
31+ from PyQt6 import QtCore , QtGui , QtWidgets
32+ import shutil
33+ import sqlite3
34+ import sys
35+ import urllib .error as urllib_err
36+ import urllib .request
37+ import webbrowser
38+ import zipfile
39+ from copy import copy
40+ from pathlib import Path
41+ from typing import Any , Optional
42+
43+ from qualcoder .ai_mcp_server import AiMcpServer
44+ from qualcoder .ai_llm import get_default_ai_models , update_ai_models
45+ from qualcoder .ai_runtime import (
46+ AI_DISABLED ,
47+ AI_INITIALIZING ,
48+ AI_READY ,
49+ AI_UNLOADED ,
50+ VECTORSTORE_DISABLED ,
51+ VECTORSTORE_UNLOADED ,
52+ vectorstore_required ,
53+ )
54+ from qualcoder .helpers import get_default_user_directory , Message
55+ from qualcoder .speakers import speaker_coder_name
5756
5857qc_config_folder = Path ('~' ).expanduser () / '.qualcoder'
5958logger = logging .getLogger (__name__ )
@@ -117,7 +116,7 @@ class App(object):
117116 """
118117
119118 def __init__ (self ):
120- self .version = "QualCoder 4.0 Beta " # Must start with 'QualCoder '
119+ self .version = "QualCoder 4.0" # Must start with 'QualCoder '
121120 self .citation = f"Citation:\n Curtain C, Dröge K, Missaghieh--Poncet J, Salomón L. (2026) { self .version } [Computer software].\n "
122121 self .citation += f"Retrieved from https://github.com/ccbogel/QualCoder/releases/tag/{ self .version } "
123122 self .conn = None
@@ -131,85 +130,85 @@ def __init__(self):
131130 self .confighome = str (qc_config_folder )
132131 self .configpath = str (qc_config_folder / 'config.ini' )
133132 self .persist_path = str (qc_config_folder / 'recent_projects.txt' )
134- self .pending_ai_model_upgrade_offer = None
135- self .ai_runtime_error = ""
136- self .settings , self .ai_models = self .load_settings ()
137- self .ai_runtime_state = (
138- AI_UNLOADED if self .settings ['ai_enable' ] == 'True' else AI_DISABLED
139- )
133+ self .pending_ai_model_upgrade_offer = None
134+ self .ai_runtime_error = ""
135+ self .settings , self .ai_models = self .load_settings ()
136+ self .ai_runtime_state = (
137+ AI_UNLOADED if self .settings ['ai_enable' ] == 'True' else AI_DISABLED
138+ )
140139 self .last_export_directory = copy (self .settings ['directory' ])
141- self .ai = None
142- # The project vectorstore is shared by AI and external MCP. Expensive
143- # dependencies are imported only when a consumer constructs the store.
144- self .vectorstore = None
145- self .vectorstore_runtime_state = (
146- VECTORSTORE_UNLOADED if vectorstore_required (self ) else VECTORSTORE_DISABLED
147- )
148- self .vectorstore_runtime_error = ""
149- # Sentence transformer embedding function. It is stored here so it must not be reloaded every time a project is opened.
150- self .ai_embedding_function = None
151- self .project_events = ProjectEventBus ()
152- self .ai_mcp_server = AiMcpServer (self )
153-
154- def get_ai_status (self ) -> str :
155- """Return one status covering both runtime loading and AI operation."""
156-
157- if self .ai_runtime_state != AI_READY :
158- return self .ai_runtime_state
159- if self .ai is None :
160- return AI_INITIALIZING
161- return self .ai .get_status ()
162-
163- def _active_vectorstore (self ) -> Optional [Any ]:
164- """Return the shared vectorstore when a configured consumer needs it."""
165-
166- if not vectorstore_required (self ):
167- return None
168- return self .vectorstore
169-
170- def vectorstore_import_document (
171- self , source_id : int , source_name : str , text : Optional [str ]) -> None :
172- """Queue one changed source document for indexing when search is active.
173-
174- Args:
175- source_id: Project source id.
176- source_name: Current source name.
177- text: Current source text.
178- """
179-
180- vectorstore = self ._active_vectorstore ()
181- if vectorstore is None :
182- return
183- try :
184- vectorstore .import_document (source_id , source_name , text )
185- except Exception as err :
186- logger .warning ("Could not update vectorstore document %s: %s" , source_id , err )
187-
188- def vectorstore_update (self ) -> None :
189- """Queue a full synchronization of the active shared vectorstore."""
190-
191- vectorstore = self ._active_vectorstore ()
192- if vectorstore is None :
193- return
194- try :
195- vectorstore .update_vectorstore ()
196- except Exception as err :
197- logger .warning ("Could not update vectorstore: %s" , err )
198-
199- def vectorstore_delete_document (self , source_id : int ) -> None :
200- """Remove one source document from the active shared vectorstore.
201-
202- Args:
203- source_id: Project source id.
204- """
205-
206- vectorstore = self ._active_vectorstore ()
207- if vectorstore is None :
208- return
209- try :
210- vectorstore .delete_document (source_id )
211- except Exception as err :
212- logger .warning ("Could not remove vectorstore document %s: %s" , source_id , err )
140+ self .ai = None
141+ # The project vectorstore is shared by AI and external MCP. Expensive
142+ # dependencies are imported only when a consumer constructs the store.
143+ self .vectorstore = None
144+ self .vectorstore_runtime_state = (
145+ VECTORSTORE_UNLOADED if vectorstore_required (self ) else VECTORSTORE_DISABLED
146+ )
147+ self .vectorstore_runtime_error = ""
148+ # Sentence transformer embedding function. It is stored here so it must not be reloaded every time a project is opened.
149+ self .ai_embedding_function = None
150+ self .project_events = ProjectEventBus ()
151+ self .ai_mcp_server = AiMcpServer (self )
152+
153+ def get_ai_status (self ) -> str :
154+ """Return one status covering both runtime loading and AI operation."""
155+
156+ if self .ai_runtime_state != AI_READY :
157+ return self .ai_runtime_state
158+ if self .ai is None :
159+ return AI_INITIALIZING
160+ return self .ai .get_status ()
161+
162+ def _active_vectorstore (self ) -> Optional [Any ]:
163+ """Return the shared vectorstore when a configured consumer needs it."""
164+
165+ if not vectorstore_required (self ):
166+ return None
167+ return self .vectorstore
168+
169+ def vectorstore_import_document (
170+ self , source_id : int , source_name : str , text : Optional [str ]) -> None :
171+ """Queue one changed source document for indexing when search is active.
172+
173+ Args:
174+ source_id: Project source id.
175+ source_name: Current source name.
176+ text: Current source text.
177+ """
178+
179+ vectorstore = self ._active_vectorstore ()
180+ if vectorstore is None :
181+ return
182+ try :
183+ vectorstore .import_document (source_id , source_name , text )
184+ except Exception as err :
185+ logger .warning ("Could not update vectorstore document %s: %s" , source_id , err )
186+
187+ def vectorstore_update (self ) -> None :
188+ """Queue a full synchronization of the active shared vectorstore."""
189+
190+ vectorstore = self ._active_vectorstore ()
191+ if vectorstore is None :
192+ return
193+ try :
194+ vectorstore .update_vectorstore ()
195+ except Exception as err :
196+ logger .warning ("Could not update vectorstore: %s" , err )
197+
198+ def vectorstore_delete_document (self , source_id : int ) -> None :
199+ """Remove one source document from the active shared vectorstore.
200+
201+ Args:
202+ source_id: Project source id.
203+ """
204+
205+ vectorstore = self ._active_vectorstore ()
206+ if vectorstore is None :
207+ return
208+ try :
209+ vectorstore .delete_document (source_id )
210+ except Exception as err :
211+ logger .warning ("Could not remove vectorstore document %s: %s" , source_id , err )
213212
214213 def read_previous_project_paths (self ):
215214 """ Recent project paths are stored in .qualcoder/recent_projects.txt
@@ -974,10 +973,10 @@ def check_and_add_additional_settings(self, settings_data, ai_models):
974973 'stylesheet' , 'backup_num' ,
975974 'report_text_context_characters' , 'report_text_context_style' ,
976975 'ai_enable' , 'ai_first_startup' , 'ai_model_index' , 'ai_chat_sidebar' ,
977- 'ai_permissions' , 'ai_extended_logging' , 'ai_model_upgrade_offers_seen' ,
978- 'ai_model_upgrade_offer_pending' ,
979- 'mcp_external_enabled' , 'external_mcp_notice_acknowledged' , 'mcp_external_port' ,
980- 'ai_chat_sidebar_width' , 'ai_chat_splitter_output_bottom'
976+ 'ai_permissions' , 'ai_extended_logging' , 'ai_model_upgrade_offers_seen' ,
977+ 'ai_model_upgrade_offer_pending' ,
978+ 'mcp_external_enabled' , 'external_mcp_notice_acknowledged' , 'mcp_external_port' ,
979+ 'ai_chat_sidebar_width' , 'ai_chat_splitter_output_bottom'
981980 ]
982981 for key in keys :
983982 if key not in settings_data :
@@ -1013,16 +1012,16 @@ def check_and_add_additional_settings(self, settings_data, ai_models):
10131012 settings_data [key ] = 1
10141013 if key == 'ai_extended_logging' :
10151014 settings_data [key ] = 'False'
1016- if key == 'ai_model_upgrade_offers_seen' :
1017- settings_data [key ] = ''
1018- if key == 'ai_model_upgrade_offer_pending' :
1019- settings_data [key ] = ''
1020- if key == 'mcp_external_enabled' :
1021- settings_data [key ] = 'False'
1022- if key == 'external_mcp_notice_acknowledged' :
1023- settings_data [key ] = 'False'
1024- if key == 'mcp_external_port' :
1025- settings_data [key ] = 47363
1015+ if key == 'ai_model_upgrade_offers_seen' :
1016+ settings_data [key ] = ''
1017+ if key == 'ai_model_upgrade_offer_pending' :
1018+ settings_data [key ] = ''
1019+ if key == 'mcp_external_enabled' :
1020+ settings_data [key ] = 'False'
1021+ if key == 'external_mcp_notice_acknowledged' :
1022+ settings_data [key ] = 'False'
1023+ if key == 'mcp_external_port' :
1024+ settings_data [key ] = 47363
10261025 if key == 'ai_chat_sidebar' :
10271026 settings_data [key ] = 'False'
10281027 if key == 'ai_chat_sidebar_width' :
@@ -1036,18 +1035,18 @@ def check_and_add_additional_settings(self, settings_data, ai_models):
10361035 if key == 'dialogcodepdf_page_view' :
10371036 settings_data [key ] = 0
10381037
1039- ai_permissions = settings_data .get ('ai_permissions' , 1 )
1040- if ai_permissions not in (0 , 1 , 2 ):
1041- settings_data ['ai_permissions' ] = 1
1042- settings_updated = True
1043-
1044- mcp_enabled = str (settings_data .get ('mcp_external_enabled' , 'False' )).lower () == 'true'
1045- notice_acknowledged = str (
1046- settings_data .get ('external_mcp_notice_acknowledged' , 'False' )
1047- ).lower () == 'true'
1048- if mcp_enabled and not notice_acknowledged :
1049- settings_data ['mcp_external_enabled' ] = 'False'
1050- settings_updated = True
1038+ ai_permissions = settings_data .get ('ai_permissions' , 1 )
1039+ if ai_permissions not in (0 , 1 , 2 ):
1040+ settings_data ['ai_permissions' ] = 1
1041+ settings_updated = True
1042+
1043+ mcp_enabled = str (settings_data .get ('mcp_external_enabled' , 'False' )).lower () == 'true'
1044+ notice_acknowledged = str (
1045+ settings_data .get ('external_mcp_notice_acknowledged' , 'False' )
1046+ ).lower () == 'true'
1047+ if mcp_enabled and not notice_acknowledged :
1048+ settings_data ['mcp_external_enabled' ] = 'False'
1049+ settings_updated = True
10511050
10521051 # Check AI models
10531052 if len (ai_models ) == 0 : # No models loaded, create default
@@ -1306,16 +1305,16 @@ def highlight_color(self):
13061305 return "#303f9f"
13071306 if self .settings ['stylesheet' ] == "purple" :
13081307 return "#ca1b9a"
1309- if self .settings ['stylesheet' ] == "native" :
1310- palette = QtWidgets .QApplication .instance ().palette ()
1311- color_role = QtGui .QPalette .ColorRole .Highlight
1312- if platform .system () == "Darwin" :
1313- color_role = getattr (
1314- QtGui .QPalette .ColorRole ,
1315- "Accent" ,
1316- QtGui .QPalette .ColorRole .Highlight ,
1317- )
1318- return palette .color (color_role ).name (QtGui .QColor .NameFormat .HexRgb )
1308+ if self .settings ['stylesheet' ] == "native" :
1309+ palette = QtWidgets .QApplication .instance ().palette ()
1310+ color_role = QtGui .QPalette .ColorRole .Highlight
1311+ if platform .system () == "Darwin" :
1312+ color_role = getattr (
1313+ QtGui .QPalette .ColorRole ,
1314+ "Accent" ,
1315+ QtGui .QPalette .ColorRole .Highlight ,
1316+ )
1317+ return palette .color (color_role ).name (QtGui .QColor .NameFormat .HexRgb )
13191318 return '#f89407' # Default
13201319
13211320 def qtawesome_icon_color (self ):
@@ -1453,13 +1452,13 @@ def default_settings(self):
14531452 'ai_first_startup' : 'True' ,
14541453 'ai_model_index' : - 1 ,
14551454 'ai_permissions' : 1 ,
1456- 'ai_extended_logging' : 'False' ,
1457- 'ai_model_upgrade_offers_seen' : '' ,
1458- 'ai_model_upgrade_offer_pending' : '' ,
1459- 'mcp_external_enabled' : 'False' ,
1460- 'external_mcp_notice_acknowledged' : 'False' ,
1461- 'mcp_external_port' : 47363 ,
1462- 'ai_chat_sidebar' : 'False' ,
1455+ 'ai_extended_logging' : 'False' ,
1456+ 'ai_model_upgrade_offers_seen' : '' ,
1457+ 'ai_model_upgrade_offer_pending' : '' ,
1458+ 'mcp_external_enabled' : 'False' ,
1459+ 'external_mcp_notice_acknowledged' : 'False' ,
1460+ 'mcp_external_port' : 47363 ,
1461+ 'ai_chat_sidebar' : 'False' ,
14631462 'ai_chat_sidebar_width' : 320 ,
14641463 'ai_chat_splitter_output_bottom' : 80
14651464 }
0 commit comments