abstractassistant 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractassistant/app.py +290 -179
- abstractassistant/core/llm_manager.py +189 -19
- abstractassistant/core/tts_manager.py +75 -25
- abstractassistant/ui/history_dialog.py +5 -5
- abstractassistant/ui/qt_bubble.py +291 -121
- abstractassistant/ui/toast_window.py +14 -15
- abstractassistant/ui/ui_styles.py +2 -2
- abstractassistant/utils/icon_generator.py +269 -139
- abstractassistant/utils/markdown_renderer.py +1 -1
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.0.dist-info}/METADATA +12 -14
- abstractassistant-0.3.0.dist-info/RECORD +28 -0
- setup_macos_app.py +64 -10
- abstractassistant-0.2.7.dist-info/RECORD +0 -28
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.0.dist-info}/WHEEL +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -31,7 +31,7 @@ try:
|
|
|
31
31
|
QTextEdit, QPushButton, QComboBox, QLabel, QFrame,
|
|
32
32
|
QFileDialog, QMessageBox
|
|
33
33
|
)
|
|
34
|
-
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, pyqtSlot, QRect
|
|
34
|
+
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, pyqtSlot, QRect, QMetaObject
|
|
35
35
|
from PyQt5.QtGui import QFont, QPalette, QColor, QPainter, QPen, QBrush
|
|
36
36
|
from PyQt5.QtCore import QPoint
|
|
37
37
|
QT_AVAILABLE = "PyQt5"
|
|
@@ -42,7 +42,7 @@ except ImportError:
|
|
|
42
42
|
QTextEdit, QPushButton, QComboBox, QLabel, QFrame,
|
|
43
43
|
QFileDialog, QMessageBox
|
|
44
44
|
)
|
|
45
|
-
from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal, QThread, Slot as pyqtSlot
|
|
45
|
+
from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal, QThread, Slot as pyqtSlot, QMetaObject
|
|
46
46
|
from PySide2.QtGui import QFont, QPalette, QColor, QPainter, QPen, QBrush
|
|
47
47
|
from PySide2.QtCore import QPoint
|
|
48
48
|
QT_AVAILABLE = "PySide2"
|
|
@@ -145,7 +145,7 @@ class TTSToggle(QPushButton):
|
|
|
145
145
|
border-radius: 12px;
|
|
146
146
|
font-size: 12px;
|
|
147
147
|
color: {text_color};
|
|
148
|
-
font-family: "
|
|
148
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
149
149
|
font-weight: 600;
|
|
150
150
|
}}
|
|
151
151
|
QPushButton:hover {{
|
|
@@ -212,7 +212,7 @@ class FullVoiceToggle(QPushButton):
|
|
|
212
212
|
border-radius: 12px;
|
|
213
213
|
font-size: 12px;
|
|
214
214
|
color: {text_color};
|
|
215
|
-
font-family: "
|
|
215
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
216
216
|
font-weight: 600;
|
|
217
217
|
}}
|
|
218
218
|
QPushButton:hover {{
|
|
@@ -257,7 +257,8 @@ class LLMWorker(QThread):
|
|
|
257
257
|
self.response_ready.emit(response_text)
|
|
258
258
|
|
|
259
259
|
except Exception as e:
|
|
260
|
-
|
|
260
|
+
if self.debug:
|
|
261
|
+
print(f"❌ LLM Error: {e}")
|
|
261
262
|
import traceback
|
|
262
263
|
traceback.print_exc()
|
|
263
264
|
self.error_occurred.emit(str(e))
|
|
@@ -331,6 +332,24 @@ class QtChatBubble(QWidget):
|
|
|
331
332
|
if self.debug:
|
|
332
333
|
print("✅ QtChatBubble initialized")
|
|
333
334
|
|
|
335
|
+
def set_response_callback(self, callback):
|
|
336
|
+
"""Set response callback."""
|
|
337
|
+
self.response_callback = callback
|
|
338
|
+
|
|
339
|
+
def set_error_callback(self, callback):
|
|
340
|
+
"""Set error callback."""
|
|
341
|
+
self.error_callback = callback
|
|
342
|
+
|
|
343
|
+
def set_status_callback(self, callback):
|
|
344
|
+
"""Set status callback."""
|
|
345
|
+
self.status_callback = callback
|
|
346
|
+
if self.debug:
|
|
347
|
+
print("✅ Status callback set in QtChatBubble")
|
|
348
|
+
|
|
349
|
+
def set_app_quit_callback(self, callback):
|
|
350
|
+
"""Set app quit callback."""
|
|
351
|
+
self.app_quit_callback = callback
|
|
352
|
+
|
|
334
353
|
def setup_ui(self):
|
|
335
354
|
"""Set up the modern user interface with SOTA UX practices."""
|
|
336
355
|
self.setWindowTitle("AbstractAssistant")
|
|
@@ -366,7 +385,7 @@ class QtChatBubble(QWidget):
|
|
|
366
385
|
font-size: 14px;
|
|
367
386
|
font-weight: 600;
|
|
368
387
|
color: rgba(255, 255, 255, 0.9);
|
|
369
|
-
font-family: "
|
|
388
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
370
389
|
}
|
|
371
390
|
QPushButton:hover {
|
|
372
391
|
background: rgba(255, 60, 60, 0.8);
|
|
@@ -398,7 +417,7 @@ class QtChatBubble(QWidget):
|
|
|
398
417
|
border-radius: 11px;
|
|
399
418
|
font-size: 10px;
|
|
400
419
|
color: rgba(255, 255, 255, 0.7);
|
|
401
|
-
font-family: "
|
|
420
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
402
421
|
padding: 0 10px;
|
|
403
422
|
}
|
|
404
423
|
QPushButton:hover {
|
|
@@ -440,7 +459,7 @@ class QtChatBubble(QWidget):
|
|
|
440
459
|
font-size: 10px;
|
|
441
460
|
font-weight: 600;
|
|
442
461
|
color: #ffffff;
|
|
443
|
-
font-family: "
|
|
462
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
444
463
|
}
|
|
445
464
|
""")
|
|
446
465
|
header_layout.addWidget(self.status_label)
|
|
@@ -570,7 +589,7 @@ class QtChatBubble(QWidget):
|
|
|
570
589
|
padding: 0 8px;
|
|
571
590
|
font-size: 11px;
|
|
572
591
|
color: rgba(255, 255, 255, 0.9);
|
|
573
|
-
font-family: "
|
|
592
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
574
593
|
}
|
|
575
594
|
QComboBox:hover {
|
|
576
595
|
background: rgba(255, 255, 255, 0.12);
|
|
@@ -600,7 +619,7 @@ class QtChatBubble(QWidget):
|
|
|
600
619
|
padding: 0 8px;
|
|
601
620
|
font-size: 11px;
|
|
602
621
|
color: rgba(255, 255, 255, 0.9);
|
|
603
|
-
font-family: "
|
|
622
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
604
623
|
}
|
|
605
624
|
QComboBox:hover {
|
|
606
625
|
background: rgba(255, 255, 255, 0.12);
|
|
@@ -631,7 +650,7 @@ class QtChatBubble(QWidget):
|
|
|
631
650
|
border-radius: 14px;
|
|
632
651
|
font-size: 12px;
|
|
633
652
|
color: rgba(255, 255, 255, 0.6);
|
|
634
|
-
font-family: "
|
|
653
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
635
654
|
}
|
|
636
655
|
""")
|
|
637
656
|
controls_layout.addWidget(self.token_label)
|
|
@@ -672,7 +691,7 @@ class QtChatBubble(QWidget):
|
|
|
672
691
|
font-size: 14px;
|
|
673
692
|
font-weight: 400;
|
|
674
693
|
color: #ffffff;
|
|
675
|
-
font-family: "
|
|
694
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
676
695
|
selection-background-color: #0066cc;
|
|
677
696
|
line-height: 1.4;
|
|
678
697
|
}
|
|
@@ -695,7 +714,7 @@ class QtChatBubble(QWidget):
|
|
|
695
714
|
font-size: 11px;
|
|
696
715
|
font-weight: 500;
|
|
697
716
|
color: #ffffff;
|
|
698
|
-
font-family: "
|
|
717
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
699
718
|
}
|
|
700
719
|
|
|
701
720
|
QPushButton:hover {
|
|
@@ -723,7 +742,7 @@ class QtChatBubble(QWidget):
|
|
|
723
742
|
font-size: 12px;
|
|
724
743
|
font-weight: 400;
|
|
725
744
|
color: #ffffff;
|
|
726
|
-
font-family: "
|
|
745
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
727
746
|
letter-spacing: 0.01em;
|
|
728
747
|
}
|
|
729
748
|
|
|
@@ -753,7 +772,7 @@ class QtChatBubble(QWidget):
|
|
|
753
772
|
selection-background-color: #4299e1;
|
|
754
773
|
color: #e2e8f0;
|
|
755
774
|
padding: 4px;
|
|
756
|
-
font-family: "
|
|
775
|
+
font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
|
|
757
776
|
}
|
|
758
777
|
|
|
759
778
|
QComboBox QAbstractItemView::item {
|
|
@@ -785,7 +804,7 @@ class QtChatBubble(QWidget):
|
|
|
785
804
|
color: rgba(255, 255, 255, 0.8);
|
|
786
805
|
font-size: 12px;
|
|
787
806
|
font-weight: 500;
|
|
788
|
-
font-family: "
|
|
807
|
+
font-family: "Helvetica Neue", "Helvetica", 'Segoe UI', Arial, sans-serif;
|
|
789
808
|
letter-spacing: 0.3px;
|
|
790
809
|
}
|
|
791
810
|
|
|
@@ -800,7 +819,7 @@ class QtChatBubble(QWidget):
|
|
|
800
819
|
text-transform: uppercase;
|
|
801
820
|
letter-spacing: 0.5px;
|
|
802
821
|
color: #a6e3a1;
|
|
803
|
-
font-family: "
|
|
822
|
+
font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
|
|
804
823
|
}
|
|
805
824
|
|
|
806
825
|
QLabel#status_generating {
|
|
@@ -813,7 +832,7 @@ class QtChatBubble(QWidget):
|
|
|
813
832
|
text-transform: uppercase;
|
|
814
833
|
letter-spacing: 0.5px;
|
|
815
834
|
color: #fab387;
|
|
816
|
-
font-family: "
|
|
835
|
+
font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
|
|
817
836
|
}
|
|
818
837
|
|
|
819
838
|
QLabel#status_error {
|
|
@@ -826,7 +845,7 @@ class QtChatBubble(QWidget):
|
|
|
826
845
|
text-transform: uppercase;
|
|
827
846
|
letter-spacing: 0.5px;
|
|
828
847
|
color: #f38ba8;
|
|
829
|
-
font-family: "
|
|
848
|
+
font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
|
|
830
849
|
}
|
|
831
850
|
|
|
832
851
|
QLabel#token_label {
|
|
@@ -834,7 +853,7 @@ class QtChatBubble(QWidget):
|
|
|
834
853
|
border: 1px solid #4a5568;
|
|
835
854
|
border-radius: 8px;
|
|
836
855
|
padding: 10px 12px;
|
|
837
|
-
font-family: "
|
|
856
|
+
font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
|
|
838
857
|
font-size: 11px;
|
|
839
858
|
font-weight: 500;
|
|
840
859
|
color: #cbd5e0;
|
|
@@ -868,7 +887,8 @@ class QtChatBubble(QWidget):
|
|
|
868
887
|
self.move(x, y)
|
|
869
888
|
|
|
870
889
|
if self.debug:
|
|
871
|
-
|
|
890
|
+
if self.debug:
|
|
891
|
+
print(f"Positioned bubble at ({x}, {y})")
|
|
872
892
|
|
|
873
893
|
def load_providers(self):
|
|
874
894
|
"""Load available providers using ProviderManager."""
|
|
@@ -881,13 +901,15 @@ class QtChatBubble(QWidget):
|
|
|
881
901
|
available_providers = self.provider_manager.get_available_providers(exclude_mock=True)
|
|
882
902
|
|
|
883
903
|
if self.debug:
|
|
884
|
-
|
|
904
|
+
if self.debug:
|
|
905
|
+
print(f"🔍 ProviderManager found {len(available_providers)} available providers")
|
|
885
906
|
|
|
886
907
|
# Add providers to dropdown
|
|
887
908
|
for display_name, provider_key in available_providers:
|
|
888
909
|
self.provider_combo.addItem(display_name, provider_key)
|
|
889
910
|
if self.debug:
|
|
890
|
-
|
|
911
|
+
if self.debug:
|
|
912
|
+
print(f" ✅ Added: {display_name} ({provider_key})")
|
|
891
913
|
|
|
892
914
|
# Set preferred provider
|
|
893
915
|
preferred = self.provider_manager.get_preferred_provider(available_providers, 'lmstudio')
|
|
@@ -924,14 +946,16 @@ class QtChatBubble(QWidget):
|
|
|
924
946
|
)
|
|
925
947
|
|
|
926
948
|
if self.debug:
|
|
927
|
-
|
|
949
|
+
if self.debug:
|
|
950
|
+
print(f"🔍 Final selected provider: {self.current_provider}")
|
|
928
951
|
|
|
929
952
|
# Load models for current provider
|
|
930
953
|
self.update_models()
|
|
931
954
|
|
|
932
955
|
except Exception as e:
|
|
933
956
|
if self.debug:
|
|
934
|
-
|
|
957
|
+
if self.debug:
|
|
958
|
+
print(f"❌ Error loading providers: {e}")
|
|
935
959
|
import traceback
|
|
936
960
|
traceback.print_exc()
|
|
937
961
|
|
|
@@ -940,7 +964,8 @@ class QtChatBubble(QWidget):
|
|
|
940
964
|
self.provider_combo.addItem("LMStudio (Local)", "lmstudio")
|
|
941
965
|
self.current_provider = "lmstudio"
|
|
942
966
|
if self.debug:
|
|
943
|
-
|
|
967
|
+
if self.debug:
|
|
968
|
+
print("🔄 Using fallback provider list")
|
|
944
969
|
|
|
945
970
|
def update_models(self):
|
|
946
971
|
"""Update model dropdown using ProviderManager."""
|
|
@@ -952,7 +977,8 @@ class QtChatBubble(QWidget):
|
|
|
952
977
|
models = self.provider_manager.get_models_for_provider(self.current_provider)
|
|
953
978
|
|
|
954
979
|
if self.debug:
|
|
955
|
-
|
|
980
|
+
if self.debug:
|
|
981
|
+
print(f"📋 ProviderManager loaded {len(models)} models for {self.current_provider}")
|
|
956
982
|
|
|
957
983
|
# Add models to dropdown with display names
|
|
958
984
|
for model in models:
|
|
@@ -994,13 +1020,15 @@ class QtChatBubble(QWidget):
|
|
|
994
1020
|
self.model_combo.setCurrentIndex(0)
|
|
995
1021
|
|
|
996
1022
|
if self.debug:
|
|
997
|
-
|
|
1023
|
+
if self.debug:
|
|
1024
|
+
print(f"✅ Final selected model: {self.current_model}")
|
|
998
1025
|
|
|
999
1026
|
self.update_token_limits()
|
|
1000
1027
|
|
|
1001
1028
|
except Exception as e:
|
|
1002
1029
|
if self.debug:
|
|
1003
|
-
|
|
1030
|
+
if self.debug:
|
|
1031
|
+
print(f"❌ Error updating models: {e}")
|
|
1004
1032
|
import traceback
|
|
1005
1033
|
traceback.print_exc()
|
|
1006
1034
|
|
|
@@ -1010,7 +1038,8 @@ class QtChatBubble(QWidget):
|
|
|
1010
1038
|
self.current_model = "default-model"
|
|
1011
1039
|
self.model_combo.setCurrentIndex(0)
|
|
1012
1040
|
if self.debug:
|
|
1013
|
-
|
|
1041
|
+
if self.debug:
|
|
1042
|
+
print(f"🔄 Using final fallback model: {self.current_model}")
|
|
1014
1043
|
|
|
1015
1044
|
def update_token_limits(self):
|
|
1016
1045
|
"""Update token limits using AbstractCore's built-in detection."""
|
|
@@ -1060,7 +1089,8 @@ class QtChatBubble(QWidget):
|
|
|
1060
1089
|
self.update_models()
|
|
1061
1090
|
|
|
1062
1091
|
if self.debug:
|
|
1063
|
-
|
|
1092
|
+
if self.debug:
|
|
1093
|
+
print(f"Provider changed to: {self.current_provider}")
|
|
1064
1094
|
|
|
1065
1095
|
def on_model_changed(self, model_name):
|
|
1066
1096
|
"""Handle model change."""
|
|
@@ -1095,7 +1125,8 @@ class QtChatBubble(QWidget):
|
|
|
1095
1125
|
if file_path not in self.attached_files:
|
|
1096
1126
|
self.attached_files.append(file_path)
|
|
1097
1127
|
if self.debug:
|
|
1098
|
-
|
|
1128
|
+
if self.debug:
|
|
1129
|
+
print(f"📎 Attached file: {file_path}")
|
|
1099
1130
|
|
|
1100
1131
|
self.update_attached_files_display()
|
|
1101
1132
|
|
|
@@ -1181,7 +1212,8 @@ class QtChatBubble(QWidget):
|
|
|
1181
1212
|
if file_path in self.attached_files:
|
|
1182
1213
|
self.attached_files.remove(file_path)
|
|
1183
1214
|
if self.debug:
|
|
1184
|
-
|
|
1215
|
+
if self.debug:
|
|
1216
|
+
print(f"🗑️ Removed attached file: {file_path}")
|
|
1185
1217
|
self.update_attached_files_display()
|
|
1186
1218
|
|
|
1187
1219
|
def send_message(self):
|
|
@@ -1228,7 +1260,8 @@ class QtChatBubble(QWidget):
|
|
|
1228
1260
|
if self.status_callback:
|
|
1229
1261
|
self.status_callback("generating")
|
|
1230
1262
|
|
|
1231
|
-
|
|
1263
|
+
if self.debug:
|
|
1264
|
+
print("🔄 QtChatBubble: UI updated, creating worker thread...")
|
|
1232
1265
|
|
|
1233
1266
|
# 5. Start worker thread to send request with optional media files
|
|
1234
1267
|
self.worker = LLMWorker(
|
|
@@ -1241,17 +1274,20 @@ class QtChatBubble(QWidget):
|
|
|
1241
1274
|
self.worker.response_ready.connect(self.on_response_ready)
|
|
1242
1275
|
self.worker.error_occurred.connect(self.on_error_occurred)
|
|
1243
1276
|
|
|
1244
|
-
|
|
1277
|
+
if self.debug:
|
|
1278
|
+
print("🔄 QtChatBubble: Starting worker thread...")
|
|
1245
1279
|
self.worker.start()
|
|
1246
1280
|
|
|
1247
|
-
|
|
1281
|
+
if self.debug:
|
|
1282
|
+
print("🔄 QtChatBubble: Worker thread started, hiding bubble...")
|
|
1248
1283
|
# Hide bubble after sending (like the original design)
|
|
1249
1284
|
QTimer.singleShot(500, self.hide)
|
|
1250
1285
|
|
|
1251
1286
|
@pyqtSlot(str)
|
|
1252
1287
|
def on_response_ready(self, response):
|
|
1253
1288
|
"""Handle LLM response."""
|
|
1254
|
-
|
|
1289
|
+
if self.debug:
|
|
1290
|
+
print(f"✅ QtChatBubble: on_response_ready called with response: {response[:100]}...")
|
|
1255
1291
|
|
|
1256
1292
|
self.send_button.setEnabled(True)
|
|
1257
1293
|
self.send_button.setText("→")
|
|
@@ -1271,6 +1307,10 @@ class QtChatBubble(QWidget):
|
|
|
1271
1307
|
}
|
|
1272
1308
|
""")
|
|
1273
1309
|
|
|
1310
|
+
# Notify main app about status change (for icon animation)
|
|
1311
|
+
if self.status_callback:
|
|
1312
|
+
self.status_callback("ready")
|
|
1313
|
+
|
|
1274
1314
|
# Get updated message history from AbstractCore session
|
|
1275
1315
|
self._update_message_history_from_session()
|
|
1276
1316
|
|
|
@@ -1280,53 +1320,83 @@ class QtChatBubble(QWidget):
|
|
|
1280
1320
|
# Handle TTS if enabled (AbstractVoice integration)
|
|
1281
1321
|
if self.tts_enabled and self.voice_manager and self.voice_manager.is_available():
|
|
1282
1322
|
if self.debug:
|
|
1283
|
-
|
|
1323
|
+
if self.debug:
|
|
1324
|
+
print("🔊 TTS enabled, speaking response...")
|
|
1284
1325
|
|
|
1285
1326
|
# Don't show toast when TTS is enabled
|
|
1286
1327
|
try:
|
|
1287
1328
|
# Clean response for voice synthesis
|
|
1288
1329
|
clean_response = self._clean_response_for_voice(response)
|
|
1289
1330
|
|
|
1331
|
+
# Set up callbacks to detect when speech actually starts/ends
|
|
1332
|
+
# Use QMetaObject.invokeMethod to ensure callbacks run on main thread
|
|
1333
|
+
def on_speech_start():
|
|
1334
|
+
if self.debug:
|
|
1335
|
+
print("🔊 QtChatBubble: Speech actually started (background thread)")
|
|
1336
|
+
# Schedule status update on main thread
|
|
1337
|
+
QMetaObject.invokeMethod(self, "_on_speech_started_main_thread", Qt.QueuedConnection)
|
|
1338
|
+
|
|
1339
|
+
def on_speech_end():
|
|
1340
|
+
if self.debug:
|
|
1341
|
+
print("🔊 QtChatBubble: Speech ended (background thread)")
|
|
1342
|
+
# Schedule completion handling on main thread
|
|
1343
|
+
QMetaObject.invokeMethod(self, "_on_speech_ended_main_thread", Qt.QueuedConnection)
|
|
1344
|
+
|
|
1345
|
+
# Set the callbacks on the voice manager
|
|
1346
|
+
self.voice_manager.on_speech_start = on_speech_start
|
|
1347
|
+
self.voice_manager.on_speech_end = on_speech_end
|
|
1348
|
+
|
|
1290
1349
|
# Speak the cleaned response using AbstractVoice-compatible interface
|
|
1350
|
+
# Note: We don't set "speaking" status here anymore - we wait for the callback
|
|
1291
1351
|
self.voice_manager.speak(clean_response)
|
|
1292
1352
|
|
|
1293
1353
|
# Update toggle state to 'speaking'
|
|
1294
1354
|
self._update_tts_toggle_state()
|
|
1295
|
-
|
|
1296
|
-
# Wait for speech to complete in a separate thread
|
|
1297
|
-
def wait_for_speech():
|
|
1298
|
-
while self.voice_manager.is_speaking():
|
|
1299
|
-
time.sleep(0.1)
|
|
1300
|
-
# Update toggle state when speech completes
|
|
1301
|
-
self._update_tts_toggle_state()
|
|
1302
|
-
if self.debug:
|
|
1303
|
-
print("🔊 TTS completed")
|
|
1304
1355
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1356
|
+
# Store response for callback when TTS completes
|
|
1357
|
+
self._pending_response = response
|
|
1307
1358
|
|
|
1308
1359
|
# Show chat history after TTS starts (small delay) - only if voice mode is OFF
|
|
1309
1360
|
QTimer.singleShot(800, self._show_history_if_voice_mode_off)
|
|
1310
1361
|
|
|
1311
1362
|
except Exception as e:
|
|
1312
1363
|
if self.debug:
|
|
1313
|
-
|
|
1364
|
+
if self.debug:
|
|
1365
|
+
print(f"❌ TTS error: {e}")
|
|
1314
1366
|
# Show chat history as fallback - only if voice mode is OFF
|
|
1315
1367
|
QTimer.singleShot(100, self._show_history_if_voice_mode_off)
|
|
1316
1368
|
else:
|
|
1317
1369
|
# Show chat history instead of toast when TTS is disabled - only if voice mode is OFF
|
|
1318
1370
|
self._show_history_if_voice_mode_off()
|
|
1319
1371
|
|
|
1320
|
-
#
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
self.
|
|
1372
|
+
# Handle status transitions based on TTS mode
|
|
1373
|
+
tts_will_handle = self.tts_enabled and self.voice_manager and self.voice_manager.is_available()
|
|
1374
|
+
if self.debug:
|
|
1375
|
+
print(f"🔍 QtChatBubble: TTS decision - tts_enabled={self.tts_enabled}, voice_manager={self.voice_manager is not None}, is_available={self.voice_manager.is_available() if self.voice_manager else False}")
|
|
1376
|
+
print(f"🔍 QtChatBubble: TTS will handle callbacks: {tts_will_handle}")
|
|
1377
|
+
|
|
1378
|
+
if not tts_will_handle:
|
|
1379
|
+
# Non-TTS path: Go directly to ready mode
|
|
1380
|
+
if self.debug:
|
|
1381
|
+
print(f"🔄 QtChatBubble: Non-TTS path - going to ready mode immediately")
|
|
1382
|
+
if self.response_callback:
|
|
1383
|
+
self.response_callback(response)
|
|
1384
|
+
if self.status_callback:
|
|
1385
|
+
self.status_callback("ready")
|
|
1386
|
+
else:
|
|
1387
|
+
# TTS path: Stay in thinking mode until audio actually starts
|
|
1388
|
+
if self.debug:
|
|
1389
|
+
print(f"🔊 QtChatBubble: TTS path - staying in thinking mode until audio starts")
|
|
1390
|
+
print(f"🔊 QtChatBubble: v0.5.1 callbacks will handle status transitions")
|
|
1391
|
+
# DON'T call response_callback or set "ready" status here!
|
|
1392
|
+
# The v0.5.1 callbacks will handle everything
|
|
1324
1393
|
|
|
1325
1394
|
def on_tts_toggled(self, enabled: bool):
|
|
1326
1395
|
"""Handle TTS toggle state change."""
|
|
1327
1396
|
self.tts_enabled = enabled
|
|
1328
1397
|
if self.debug:
|
|
1329
|
-
|
|
1398
|
+
if self.debug:
|
|
1399
|
+
print(f"🔊 TTS {'enabled' if enabled else 'disabled'}")
|
|
1330
1400
|
|
|
1331
1401
|
# Stop any current speech when disabling
|
|
1332
1402
|
if not enabled and self.voice_manager:
|
|
@@ -1335,17 +1405,20 @@ class QtChatBubble(QWidget):
|
|
|
1335
1405
|
self._update_tts_toggle_state()
|
|
1336
1406
|
except Exception as e:
|
|
1337
1407
|
if self.debug:
|
|
1338
|
-
|
|
1408
|
+
if self.debug:
|
|
1409
|
+
print(f"❌ Error stopping TTS: {e}")
|
|
1339
1410
|
|
|
1340
|
-
# Update LLM session
|
|
1411
|
+
# Update LLM session mode while preserving chat history
|
|
1341
1412
|
if self.llm_manager:
|
|
1342
1413
|
try:
|
|
1343
|
-
self.llm_manager.
|
|
1414
|
+
self.llm_manager.update_session_mode(tts_mode=enabled)
|
|
1344
1415
|
if self.debug:
|
|
1345
|
-
|
|
1416
|
+
if self.debug:
|
|
1417
|
+
print(f"🔄 LLM session mode updated for {'TTS' if enabled else 'normal'} mode (history preserved)")
|
|
1346
1418
|
except Exception as e:
|
|
1347
1419
|
if self.debug:
|
|
1348
|
-
|
|
1420
|
+
if self.debug:
|
|
1421
|
+
print(f"❌ Error updating LLM session: {e}")
|
|
1349
1422
|
|
|
1350
1423
|
def on_tts_single_click(self):
|
|
1351
1424
|
"""Handle single click on TTS toggle - pause/resume functionality."""
|
|
@@ -1359,27 +1432,33 @@ class QtChatBubble(QWidget):
|
|
|
1359
1432
|
# Pause the speech - may need multiple attempts if audio stream just started
|
|
1360
1433
|
success = self._attempt_pause_with_retry()
|
|
1361
1434
|
if success and self.debug:
|
|
1362
|
-
|
|
1435
|
+
if self.debug:
|
|
1436
|
+
print("🔊 TTS paused via single click")
|
|
1363
1437
|
elif self.debug:
|
|
1364
|
-
|
|
1438
|
+
if self.debug:
|
|
1439
|
+
print("🔊 TTS pause failed - audio stream may not be ready yet")
|
|
1365
1440
|
elif current_state == 'paused':
|
|
1366
1441
|
# Resume the speech
|
|
1367
1442
|
success = self.voice_manager.resume()
|
|
1368
1443
|
if success and self.debug:
|
|
1369
|
-
|
|
1444
|
+
if self.debug:
|
|
1445
|
+
print("🔊 TTS resumed via single click")
|
|
1370
1446
|
elif self.debug:
|
|
1371
|
-
|
|
1447
|
+
if self.debug:
|
|
1448
|
+
print("🔊 TTS resume failed")
|
|
1372
1449
|
else:
|
|
1373
1450
|
# If idle, do nothing or could show a message
|
|
1374
1451
|
if self.debug:
|
|
1375
|
-
|
|
1452
|
+
if self.debug:
|
|
1453
|
+
print("🔊 TTS single click - no active speech to pause/resume")
|
|
1376
1454
|
|
|
1377
1455
|
# Update visual state
|
|
1378
1456
|
self._update_tts_toggle_state()
|
|
1379
1457
|
|
|
1380
1458
|
except Exception as e:
|
|
1381
1459
|
if self.debug:
|
|
1382
|
-
|
|
1460
|
+
if self.debug:
|
|
1461
|
+
print(f"❌ Error handling TTS single click: {e}")
|
|
1383
1462
|
|
|
1384
1463
|
def _attempt_pause_with_retry(self, max_attempts=5):
|
|
1385
1464
|
"""Attempt to pause with retry logic for timing issues.
|
|
@@ -1402,7 +1481,8 @@ class QtChatBubble(QWidget):
|
|
|
1402
1481
|
return True
|
|
1403
1482
|
|
|
1404
1483
|
if self.debug:
|
|
1405
|
-
|
|
1484
|
+
if self.debug:
|
|
1485
|
+
print(f"🔊 Pause attempt {attempt + 1}/{max_attempts} failed, retrying...")
|
|
1406
1486
|
|
|
1407
1487
|
# Short delay before retry
|
|
1408
1488
|
time.sleep(0.1)
|
|
@@ -1412,7 +1492,8 @@ class QtChatBubble(QWidget):
|
|
|
1412
1492
|
def on_tts_double_click(self):
|
|
1413
1493
|
"""Handle double click on TTS toggle - stop TTS and open chat bubble."""
|
|
1414
1494
|
if self.debug:
|
|
1415
|
-
|
|
1495
|
+
if self.debug:
|
|
1496
|
+
print("🔊 TTS double click - stopping speech and showing chat")
|
|
1416
1497
|
|
|
1417
1498
|
# Prevent double-free errors by checking if objects are still valid
|
|
1418
1499
|
try:
|
|
@@ -1429,7 +1510,8 @@ class QtChatBubble(QWidget):
|
|
|
1429
1510
|
|
|
1430
1511
|
except Exception as e:
|
|
1431
1512
|
if self.debug:
|
|
1432
|
-
|
|
1513
|
+
if self.debug:
|
|
1514
|
+
print(f"❌ Error stopping TTS on double click: {e}")
|
|
1433
1515
|
|
|
1434
1516
|
# Show the chat bubble with safety checks
|
|
1435
1517
|
if hasattr(self, 'show') and not self.isVisible():
|
|
@@ -1463,12 +1545,14 @@ class QtChatBubble(QWidget):
|
|
|
1463
1545
|
try:
|
|
1464
1546
|
# Ensure voice manager is available
|
|
1465
1547
|
if not self.voice_manager or not self.voice_manager.is_available():
|
|
1466
|
-
|
|
1548
|
+
if self.debug:
|
|
1549
|
+
print("❌ Voice manager not available for Full Voice Mode")
|
|
1467
1550
|
self.full_voice_toggle.set_enabled(False)
|
|
1468
1551
|
return
|
|
1469
1552
|
|
|
1470
1553
|
if self.debug:
|
|
1471
|
-
|
|
1554
|
+
if self.debug:
|
|
1555
|
+
print("🚀 Starting Full Voice Mode...")
|
|
1472
1556
|
|
|
1473
1557
|
# Hide text input UI
|
|
1474
1558
|
self.hide_text_ui()
|
|
@@ -1480,9 +1564,9 @@ class QtChatBubble(QWidget):
|
|
|
1480
1564
|
# Set up voice mode based on CLI parameter
|
|
1481
1565
|
self.voice_manager.set_voice_mode(self.listening_mode)
|
|
1482
1566
|
|
|
1483
|
-
# Update LLM session for voice-optimized responses
|
|
1567
|
+
# Update LLM session mode for voice-optimized responses (preserve history)
|
|
1484
1568
|
if self.llm_manager:
|
|
1485
|
-
self.llm_manager.
|
|
1569
|
+
self.llm_manager.update_session_mode(tts_mode=True)
|
|
1486
1570
|
|
|
1487
1571
|
# Start listening
|
|
1488
1572
|
self.voice_manager.listen(
|
|
@@ -1497,11 +1581,13 @@ class QtChatBubble(QWidget):
|
|
|
1497
1581
|
self.voice_manager.speak("Full voice mode activated. I'm listening...")
|
|
1498
1582
|
|
|
1499
1583
|
if self.debug:
|
|
1500
|
-
|
|
1584
|
+
if self.debug:
|
|
1585
|
+
print("✅ Full Voice Mode started successfully")
|
|
1501
1586
|
|
|
1502
1587
|
except Exception as e:
|
|
1503
1588
|
if self.debug:
|
|
1504
|
-
|
|
1589
|
+
if self.debug:
|
|
1590
|
+
print(f"❌ Error starting Full Voice Mode: {e}")
|
|
1505
1591
|
import traceback
|
|
1506
1592
|
traceback.print_exc()
|
|
1507
1593
|
|
|
@@ -1513,7 +1599,8 @@ class QtChatBubble(QWidget):
|
|
|
1513
1599
|
"""Stop Full Voice Mode and return to normal text mode."""
|
|
1514
1600
|
try:
|
|
1515
1601
|
if self.debug:
|
|
1516
|
-
|
|
1602
|
+
if self.debug:
|
|
1603
|
+
print("🛑 Stopping Full Voice Mode...")
|
|
1517
1604
|
|
|
1518
1605
|
# Stop listening
|
|
1519
1606
|
if self.voice_manager:
|
|
@@ -1527,11 +1614,13 @@ class QtChatBubble(QWidget):
|
|
|
1527
1614
|
self.update_status("READY")
|
|
1528
1615
|
|
|
1529
1616
|
if self.debug:
|
|
1530
|
-
|
|
1617
|
+
if self.debug:
|
|
1618
|
+
print("✅ Full Voice Mode stopped")
|
|
1531
1619
|
|
|
1532
1620
|
except Exception as e:
|
|
1533
1621
|
if self.debug:
|
|
1534
|
-
|
|
1622
|
+
if self.debug:
|
|
1623
|
+
print(f"❌ Error stopping Full Voice Mode: {e}")
|
|
1535
1624
|
import traceback
|
|
1536
1625
|
traceback.print_exc()
|
|
1537
1626
|
|
|
@@ -1539,7 +1628,8 @@ class QtChatBubble(QWidget):
|
|
|
1539
1628
|
"""Handle speech-to-text input from the user."""
|
|
1540
1629
|
try:
|
|
1541
1630
|
if self.debug:
|
|
1542
|
-
|
|
1631
|
+
if self.debug:
|
|
1632
|
+
print(f"👤 Voice input: {transcribed_text}")
|
|
1543
1633
|
|
|
1544
1634
|
# No longer updating voice toggle appearance - it's a simple user control
|
|
1545
1635
|
self.update_status("PROCESSING")
|
|
@@ -1555,7 +1645,8 @@ class QtChatBubble(QWidget):
|
|
|
1555
1645
|
self._update_message_history_from_session()
|
|
1556
1646
|
|
|
1557
1647
|
if self.debug:
|
|
1558
|
-
|
|
1648
|
+
if self.debug:
|
|
1649
|
+
print(f"🤖 AI response: {response[:100]}...")
|
|
1559
1650
|
|
|
1560
1651
|
# Speak the response
|
|
1561
1652
|
self.voice_manager.speak(response)
|
|
@@ -1565,7 +1656,8 @@ class QtChatBubble(QWidget):
|
|
|
1565
1656
|
|
|
1566
1657
|
except Exception as e:
|
|
1567
1658
|
if self.debug:
|
|
1568
|
-
|
|
1659
|
+
if self.debug:
|
|
1660
|
+
print(f"❌ Error handling voice input: {e}")
|
|
1569
1661
|
import traceback
|
|
1570
1662
|
traceback.print_exc()
|
|
1571
1663
|
|
|
@@ -1575,7 +1667,8 @@ class QtChatBubble(QWidget):
|
|
|
1575
1667
|
def handle_voice_stop(self):
|
|
1576
1668
|
"""Handle when user says 'stop' to exit Full Voice Mode."""
|
|
1577
1669
|
if self.debug:
|
|
1578
|
-
|
|
1670
|
+
if self.debug:
|
|
1671
|
+
print("🛑 User said 'stop' - exiting Full Voice Mode")
|
|
1579
1672
|
|
|
1580
1673
|
# Disable Full Voice Mode
|
|
1581
1674
|
self.full_voice_toggle.set_enabled(False)
|
|
@@ -1623,7 +1716,7 @@ class QtChatBubble(QWidget):
|
|
|
1623
1716
|
font-size: 10px;
|
|
1624
1717
|
font-weight: 600;
|
|
1625
1718
|
color: #ffffff;
|
|
1626
|
-
font-family: "
|
|
1719
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1627
1720
|
}}
|
|
1628
1721
|
""")
|
|
1629
1722
|
|
|
@@ -1643,10 +1736,12 @@ class QtChatBubble(QWidget):
|
|
|
1643
1736
|
self.voice_control_panel.hide()
|
|
1644
1737
|
|
|
1645
1738
|
if self.debug:
|
|
1646
|
-
|
|
1739
|
+
if self.debug:
|
|
1740
|
+
print(f"🔊 TTS toggle state updated to: {current_state}")
|
|
1647
1741
|
except Exception as e:
|
|
1648
1742
|
if self.debug:
|
|
1649
|
-
|
|
1743
|
+
if self.debug:
|
|
1744
|
+
print(f"❌ Error updating TTS toggle state: {e}")
|
|
1650
1745
|
|
|
1651
1746
|
def create_voice_control_panel(self):
|
|
1652
1747
|
"""Create a prominent voice control panel that appears when TTS is active."""
|
|
@@ -1747,11 +1842,13 @@ class QtChatBubble(QWidget):
|
|
|
1747
1842
|
self.escape_shortcut.activated.connect(self.handle_escape_shortcut)
|
|
1748
1843
|
|
|
1749
1844
|
if self.debug:
|
|
1750
|
-
|
|
1845
|
+
if self.debug:
|
|
1846
|
+
print("✅ Keyboard shortcuts setup: Space (pause/resume), Escape (stop)")
|
|
1751
1847
|
|
|
1752
1848
|
except Exception as e:
|
|
1753
1849
|
if self.debug:
|
|
1754
|
-
|
|
1850
|
+
if self.debug:
|
|
1851
|
+
print(f"❌ Error setting up keyboard shortcuts: {e}")
|
|
1755
1852
|
|
|
1756
1853
|
def handle_space_shortcut(self):
|
|
1757
1854
|
"""Handle space bar shortcut for pause/resume."""
|
|
@@ -1760,14 +1857,16 @@ class QtChatBubble(QWidget):
|
|
|
1760
1857
|
not self.input_text.hasFocus()):
|
|
1761
1858
|
self.on_tts_single_click()
|
|
1762
1859
|
if self.debug:
|
|
1763
|
-
|
|
1860
|
+
if self.debug:
|
|
1861
|
+
print("🔊 Space shortcut triggered pause/resume")
|
|
1764
1862
|
|
|
1765
1863
|
def handle_escape_shortcut(self):
|
|
1766
1864
|
"""Handle escape key shortcut for stop."""
|
|
1767
1865
|
if self.voice_manager and self.voice_manager.get_state() in ['speaking', 'paused']:
|
|
1768
1866
|
self.on_tts_double_click()
|
|
1769
1867
|
if self.debug:
|
|
1770
|
-
|
|
1868
|
+
if self.debug:
|
|
1869
|
+
print("🔊 Escape shortcut triggered stop")
|
|
1771
1870
|
|
|
1772
1871
|
def _clean_response_for_voice(self, text: str) -> str:
|
|
1773
1872
|
"""Clean response text for voice synthesis - remove formatting and make conversational."""
|
|
@@ -1817,7 +1916,8 @@ class QtChatBubble(QWidget):
|
|
|
1817
1916
|
# NO TRUNCATION - let the LLM decide response length based on system prompt
|
|
1818
1917
|
|
|
1819
1918
|
if self.debug:
|
|
1820
|
-
|
|
1919
|
+
if self.debug:
|
|
1920
|
+
print(f"🔊 Cleaned text for TTS: {text[:100]}{'...' if len(text) > 100 else ''}")
|
|
1821
1921
|
|
|
1822
1922
|
return text
|
|
1823
1923
|
|
|
@@ -1843,11 +1943,13 @@ class QtChatBubble(QWidget):
|
|
|
1843
1943
|
""")
|
|
1844
1944
|
|
|
1845
1945
|
if self.debug:
|
|
1846
|
-
|
|
1946
|
+
if self.debug:
|
|
1947
|
+
print(f"Error occurred: {error}")
|
|
1847
1948
|
|
|
1848
1949
|
# Show chat history instead of error toast
|
|
1849
1950
|
if self.debug:
|
|
1850
|
-
|
|
1951
|
+
if self.debug:
|
|
1952
|
+
print(f"❌ AI Error: {error}")
|
|
1851
1953
|
|
|
1852
1954
|
# Show history so user can see the error context - only if voice mode is OFF
|
|
1853
1955
|
QTimer.singleShot(100, self._show_history_if_voice_mode_off)
|
|
@@ -1887,14 +1989,16 @@ class QtChatBubble(QWidget):
|
|
|
1887
1989
|
if self.llm_manager:
|
|
1888
1990
|
self.llm_manager.create_new_session()
|
|
1889
1991
|
if self.debug:
|
|
1890
|
-
|
|
1992
|
+
if self.debug:
|
|
1993
|
+
print("🧹 AbstractCore session cleared and recreated")
|
|
1891
1994
|
|
|
1892
1995
|
self.message_history.clear()
|
|
1893
1996
|
self.token_count = 0
|
|
1894
1997
|
self.update_token_display()
|
|
1895
1998
|
|
|
1896
1999
|
if self.debug:
|
|
1897
|
-
|
|
2000
|
+
if self.debug:
|
|
2001
|
+
print("🧹 Session cleared")
|
|
1898
2002
|
|
|
1899
2003
|
def load_session(self):
|
|
1900
2004
|
"""Load a session using AbstractCore via LLMManager."""
|
|
@@ -1931,7 +2035,8 @@ class QtChatBubble(QWidget):
|
|
|
1931
2035
|
)
|
|
1932
2036
|
|
|
1933
2037
|
if self.debug:
|
|
1934
|
-
|
|
2038
|
+
if self.debug:
|
|
2039
|
+
print(f"📂 Loaded session via AbstractCore from {file_path}")
|
|
1935
2040
|
else:
|
|
1936
2041
|
raise Exception("Session loaded but not available in LLMManager")
|
|
1937
2042
|
else:
|
|
@@ -1944,7 +2049,8 @@ class QtChatBubble(QWidget):
|
|
|
1944
2049
|
f"Failed to load session via AbstractCore:\n{str(e)}"
|
|
1945
2050
|
)
|
|
1946
2051
|
if self.debug:
|
|
1947
|
-
|
|
2052
|
+
if self.debug:
|
|
2053
|
+
print(f"❌ Failed to load session: {e}")
|
|
1948
2054
|
|
|
1949
2055
|
def save_session(self):
|
|
1950
2056
|
"""Save the current session using AbstractCore via LLMManager."""
|
|
@@ -1980,7 +2086,8 @@ class QtChatBubble(QWidget):
|
|
|
1980
2086
|
)
|
|
1981
2087
|
|
|
1982
2088
|
if self.debug:
|
|
1983
|
-
|
|
2089
|
+
if self.debug:
|
|
2090
|
+
print(f"💾 Saved session via AbstractCore to {file_path}")
|
|
1984
2091
|
else:
|
|
1985
2092
|
raise Exception("AbstractCore session saving failed")
|
|
1986
2093
|
|
|
@@ -1991,7 +2098,8 @@ class QtChatBubble(QWidget):
|
|
|
1991
2098
|
f"Failed to save session via AbstractCore:\n{str(e)}"
|
|
1992
2099
|
)
|
|
1993
2100
|
if self.debug:
|
|
1994
|
-
|
|
2101
|
+
if self.debug:
|
|
2102
|
+
print(f"❌ Failed to save session: {e}")
|
|
1995
2103
|
|
|
1996
2104
|
def _is_voice_mode_active(self):
|
|
1997
2105
|
"""Centralized source of truth: Check if ANY voice mode is active."""
|
|
@@ -2038,11 +2146,13 @@ class QtChatBubble(QWidget):
|
|
|
2038
2146
|
self.message_history.append(message)
|
|
2039
2147
|
|
|
2040
2148
|
if self.debug:
|
|
2041
|
-
|
|
2149
|
+
if self.debug:
|
|
2150
|
+
print(f"📚 Updated message history from AbstractCore: {len(self.message_history)} messages")
|
|
2042
2151
|
|
|
2043
2152
|
except Exception as e:
|
|
2044
2153
|
if self.debug:
|
|
2045
|
-
|
|
2154
|
+
if self.debug:
|
|
2155
|
+
print(f"❌ Error updating message history from session: {e}")
|
|
2046
2156
|
|
|
2047
2157
|
def _update_token_count_from_session(self):
|
|
2048
2158
|
"""Update token count from AbstractCore session."""
|
|
@@ -2053,10 +2163,12 @@ class QtChatBubble(QWidget):
|
|
|
2053
2163
|
self.update_token_display()
|
|
2054
2164
|
|
|
2055
2165
|
if self.debug:
|
|
2056
|
-
|
|
2166
|
+
if self.debug:
|
|
2167
|
+
print(f"📊 Updated token count from AbstractCore: {self.token_count}")
|
|
2057
2168
|
except Exception as e:
|
|
2058
2169
|
if self.debug:
|
|
2059
|
-
|
|
2170
|
+
if self.debug:
|
|
2171
|
+
print(f"❌ Error updating token count from session: {e}")
|
|
2060
2172
|
|
|
2061
2173
|
def _show_history_if_voice_mode_off(self):
|
|
2062
2174
|
"""Show chat history only if voice mode is OFF."""
|
|
@@ -2125,7 +2237,7 @@ class QtChatBubble(QWidget):
|
|
|
2125
2237
|
border-radius: 11px;
|
|
2126
2238
|
font-size: 10px;
|
|
2127
2239
|
color: #ffffff;
|
|
2128
|
-
font-family: "
|
|
2240
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
2129
2241
|
padding: 0 10px;
|
|
2130
2242
|
font-weight: 600;
|
|
2131
2243
|
}
|
|
@@ -2142,7 +2254,7 @@ class QtChatBubble(QWidget):
|
|
|
2142
2254
|
border-radius: 11px;
|
|
2143
2255
|
font-size: 10px;
|
|
2144
2256
|
color: rgba(255, 255, 255, 0.7);
|
|
2145
|
-
font-family: "
|
|
2257
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
2146
2258
|
padding: 0 10px;
|
|
2147
2259
|
}
|
|
2148
2260
|
QPushButton:hover {
|
|
@@ -2154,7 +2266,8 @@ class QtChatBubble(QWidget):
|
|
|
2154
2266
|
def close_app(self):
|
|
2155
2267
|
"""Close the entire application completely."""
|
|
2156
2268
|
if self.debug:
|
|
2157
|
-
|
|
2269
|
+
if self.debug:
|
|
2270
|
+
print("🔄 Close button clicked - shutting down application")
|
|
2158
2271
|
|
|
2159
2272
|
# Stop TTS if running
|
|
2160
2273
|
if hasattr(self, 'voice_manager') and self.voice_manager:
|
|
@@ -2170,16 +2283,19 @@ class QtChatBubble(QWidget):
|
|
|
2170
2283
|
# ALWAYS try to call the app quit callback first
|
|
2171
2284
|
if hasattr(self, 'app_quit_callback') and self.app_quit_callback:
|
|
2172
2285
|
if self.debug:
|
|
2173
|
-
|
|
2286
|
+
if self.debug:
|
|
2287
|
+
print("🔄 Calling app quit callback")
|
|
2174
2288
|
try:
|
|
2175
2289
|
self.app_quit_callback()
|
|
2176
2290
|
except Exception as e:
|
|
2177
2291
|
if self.debug:
|
|
2178
|
-
|
|
2292
|
+
if self.debug:
|
|
2293
|
+
print(f"❌ App callback failed: {e}")
|
|
2179
2294
|
|
|
2180
2295
|
# ALWAYS force quit as well to ensure the app terminates
|
|
2181
2296
|
if self.debug:
|
|
2182
|
-
|
|
2297
|
+
if self.debug:
|
|
2298
|
+
print("🔄 Force quitting application")
|
|
2183
2299
|
|
|
2184
2300
|
# Get the QApplication instance
|
|
2185
2301
|
app = QApplication.instance()
|
|
@@ -2193,7 +2309,8 @@ class QtChatBubble(QWidget):
|
|
|
2193
2309
|
import sys
|
|
2194
2310
|
import os
|
|
2195
2311
|
if self.debug:
|
|
2196
|
-
|
|
2312
|
+
if self.debug:
|
|
2313
|
+
print("🔄 Force exit with sys.exit and os._exit")
|
|
2197
2314
|
try:
|
|
2198
2315
|
sys.exit(0)
|
|
2199
2316
|
except:
|
|
@@ -2204,6 +2321,53 @@ class QtChatBubble(QWidget):
|
|
|
2204
2321
|
"""Set callback to properly quit the main application."""
|
|
2205
2322
|
self.app_quit_callback = callback
|
|
2206
2323
|
|
|
2324
|
+
@pyqtSlot()
|
|
2325
|
+
def _on_speech_started_main_thread(self):
|
|
2326
|
+
"""Handle speech start on main thread (called via QMetaObject.invokeMethod)."""
|
|
2327
|
+
if self.debug:
|
|
2328
|
+
print("🔊 QtChatBubble: Speech started - updating status on main thread")
|
|
2329
|
+
if self.status_callback:
|
|
2330
|
+
self.status_callback("speaking")
|
|
2331
|
+
|
|
2332
|
+
@pyqtSlot()
|
|
2333
|
+
def _on_speech_ended_main_thread(self):
|
|
2334
|
+
"""Handle speech end on main thread (called via QMetaObject.invokeMethod)."""
|
|
2335
|
+
if self.debug:
|
|
2336
|
+
print("🔊 QtChatBubble: Speech ended - handling completion on main thread")
|
|
2337
|
+
|
|
2338
|
+
# Update toggle state when speech completes
|
|
2339
|
+
self._update_tts_toggle_state()
|
|
2340
|
+
|
|
2341
|
+
# Call response callback now that TTS is done
|
|
2342
|
+
if self.response_callback and hasattr(self, '_pending_response'):
|
|
2343
|
+
if self.debug:
|
|
2344
|
+
print(f"🔄 QtChatBubble: TTS completed, calling response callback...")
|
|
2345
|
+
self.response_callback(self._pending_response)
|
|
2346
|
+
delattr(self, '_pending_response')
|
|
2347
|
+
|
|
2348
|
+
# Notify main app that speaking is done (back to ready)
|
|
2349
|
+
if self.status_callback:
|
|
2350
|
+
if self.debug:
|
|
2351
|
+
print("🔊 QtChatBubble: Speech ended, setting ready status")
|
|
2352
|
+
self.status_callback("ready")
|
|
2353
|
+
|
|
2354
|
+
@pyqtSlot()
|
|
2355
|
+
def _execute_tts_completion_callbacks(self):
|
|
2356
|
+
"""Execute TTS completion callbacks on the main thread."""
|
|
2357
|
+
if hasattr(self, '_tts_completion_callback') and self._tts_completion_callback:
|
|
2358
|
+
if self.debug:
|
|
2359
|
+
print("🔊 QtChatBubble: Executing TTS completion callbacks on main thread...")
|
|
2360
|
+
|
|
2361
|
+
# Execute the stored callback
|
|
2362
|
+
try:
|
|
2363
|
+
self._tts_completion_callback()
|
|
2364
|
+
except Exception as e:
|
|
2365
|
+
if self.debug:
|
|
2366
|
+
print(f"❌ Error executing TTS completion callback: {e}")
|
|
2367
|
+
finally:
|
|
2368
|
+
# Clear the callback
|
|
2369
|
+
self._tts_completion_callback = None
|
|
2370
|
+
|
|
2207
2371
|
|
|
2208
2372
|
def closeEvent(self, event):
|
|
2209
2373
|
"""Handle close event."""
|
|
@@ -2217,7 +2381,8 @@ class QtChatBubble(QWidget):
|
|
|
2217
2381
|
self.voice_manager.cleanup()
|
|
2218
2382
|
except Exception as e:
|
|
2219
2383
|
if self.debug:
|
|
2220
|
-
|
|
2384
|
+
if self.debug:
|
|
2385
|
+
print(f"❌ Error cleaning up voice manager: {e}")
|
|
2221
2386
|
|
|
2222
2387
|
event.accept()
|
|
2223
2388
|
|
|
@@ -2241,20 +2406,21 @@ class QtBubbleManager:
|
|
|
2241
2406
|
raise RuntimeError("No Qt library available. Install PyQt5, PySide2, or PyQt6")
|
|
2242
2407
|
|
|
2243
2408
|
if self.debug:
|
|
2244
|
-
|
|
2409
|
+
if self.debug:
|
|
2410
|
+
print(f"✅ QtBubbleManager initialized with {QT_AVAILABLE}")
|
|
2245
2411
|
|
|
2246
2412
|
def _prepare_bubble(self):
|
|
2247
2413
|
"""Pre-initialize the bubble for instant display later."""
|
|
2248
2414
|
if not self.app:
|
|
2249
|
-
#
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
self.app = QApplication.instance()
|
|
2415
|
+
# Always use existing QApplication instance (never create a new one)
|
|
2416
|
+
self.app = QApplication.instance()
|
|
2417
|
+
if not self.app:
|
|
2418
|
+
raise RuntimeError("No QApplication instance found. This should be created by the main app first.")
|
|
2254
2419
|
|
|
2255
2420
|
if not self.bubble:
|
|
2256
2421
|
if self.debug:
|
|
2257
|
-
|
|
2422
|
+
if self.debug:
|
|
2423
|
+
print("🔄 Pre-creating QtChatBubble...")
|
|
2258
2424
|
|
|
2259
2425
|
# Create the bubble but don't show it yet
|
|
2260
2426
|
self.bubble = QtChatBubble(self.llm_manager, self.config, self.debug, self.listening_mode)
|
|
@@ -2268,7 +2434,8 @@ class QtBubbleManager:
|
|
|
2268
2434
|
self.bubble.set_status_callback(self.status_callback)
|
|
2269
2435
|
|
|
2270
2436
|
if self.debug:
|
|
2271
|
-
|
|
2437
|
+
if self.debug:
|
|
2438
|
+
print("✅ QtChatBubble pre-created and ready")
|
|
2272
2439
|
|
|
2273
2440
|
def show(self):
|
|
2274
2441
|
"""Show the chat bubble (instantly if pre-initialized)."""
|
|
@@ -2286,7 +2453,8 @@ class QtBubbleManager:
|
|
|
2286
2453
|
self.bubble.activateWindow()
|
|
2287
2454
|
|
|
2288
2455
|
if self.debug:
|
|
2289
|
-
|
|
2456
|
+
if self.debug:
|
|
2457
|
+
print("💬 Qt chat bubble shown")
|
|
2290
2458
|
|
|
2291
2459
|
def hide(self):
|
|
2292
2460
|
"""Hide the chat bubble."""
|
|
@@ -2294,7 +2462,8 @@ class QtBubbleManager:
|
|
|
2294
2462
|
self.bubble.hide()
|
|
2295
2463
|
|
|
2296
2464
|
if self.debug:
|
|
2297
|
-
|
|
2465
|
+
if self.debug:
|
|
2466
|
+
print("💬 Qt chat bubble hidden")
|
|
2298
2467
|
|
|
2299
2468
|
def destroy(self):
|
|
2300
2469
|
"""Destroy the chat bubble."""
|
|
@@ -2303,7 +2472,8 @@ class QtBubbleManager:
|
|
|
2303
2472
|
self.bubble = None
|
|
2304
2473
|
|
|
2305
2474
|
if self.debug:
|
|
2306
|
-
|
|
2475
|
+
if self.debug:
|
|
2476
|
+
print("💬 Qt chat bubble destroyed")
|
|
2307
2477
|
|
|
2308
2478
|
def set_response_callback(self, callback):
|
|
2309
2479
|
"""Set response callback."""
|