abstractassistant 0.2.7__py3-none-any.whl → 0.3.1__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 +295 -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 +379 -229
- abstractassistant/ui/toast_window.py +14 -15
- abstractassistant/ui/ui_styles.py +2 -2
- abstractassistant/utils/icon_generator.py +271 -139
- abstractassistant/utils/markdown_renderer.py +1 -1
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.1.dist-info}/METADATA +13 -15
- abstractassistant-0.3.1.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.1.dist-info}/WHEEL +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.1.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.2.7.dist-info → abstractassistant-0.3.1.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")
|
|
@@ -341,7 +360,10 @@ class QtChatBubble(QWidget):
|
|
|
341
360
|
)
|
|
342
361
|
|
|
343
362
|
# Set optimal size for modern chat interface - much wider to nearly touch screen edge
|
|
344
|
-
|
|
363
|
+
# Initial size - will be adjusted dynamically based on file attachments
|
|
364
|
+
self.base_width = 630
|
|
365
|
+
self.base_height = 196
|
|
366
|
+
self.setFixedSize(self.base_width, self.base_height)
|
|
345
367
|
self.position_near_tray()
|
|
346
368
|
|
|
347
369
|
# Main layout with minimal spacing
|
|
@@ -366,7 +388,7 @@ class QtChatBubble(QWidget):
|
|
|
366
388
|
font-size: 14px;
|
|
367
389
|
font-weight: 600;
|
|
368
390
|
color: rgba(255, 255, 255, 0.9);
|
|
369
|
-
font-family: "
|
|
391
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
370
392
|
}
|
|
371
393
|
QPushButton:hover {
|
|
372
394
|
background: rgba(255, 60, 60, 0.8);
|
|
@@ -398,7 +420,7 @@ class QtChatBubble(QWidget):
|
|
|
398
420
|
border-radius: 11px;
|
|
399
421
|
font-size: 10px;
|
|
400
422
|
color: rgba(255, 255, 255, 0.7);
|
|
401
|
-
font-family: "
|
|
423
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
402
424
|
padding: 0 10px;
|
|
403
425
|
}
|
|
404
426
|
QPushButton:hover {
|
|
@@ -421,10 +443,7 @@ class QtChatBubble(QWidget):
|
|
|
421
443
|
self.full_voice_toggle.toggled.connect(self.on_full_voice_toggled)
|
|
422
444
|
header_layout.addWidget(self.full_voice_toggle)
|
|
423
445
|
|
|
424
|
-
#
|
|
425
|
-
self.voice_control_panel = self.create_voice_control_panel()
|
|
426
|
-
header_layout.addWidget(self.voice_control_panel)
|
|
427
|
-
self.voice_control_panel.hide() # Hidden initially
|
|
446
|
+
# Voice control panel removed - not needed
|
|
428
447
|
|
|
429
448
|
header_layout.addStretch()
|
|
430
449
|
|
|
@@ -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)
|
|
@@ -546,8 +565,8 @@ class QtChatBubble(QWidget):
|
|
|
546
565
|
}
|
|
547
566
|
""")
|
|
548
567
|
self.attached_files_layout = QHBoxLayout(self.attached_files_container)
|
|
549
|
-
self.attached_files_layout.setContentsMargins(
|
|
550
|
-
self.attached_files_layout.setSpacing(
|
|
568
|
+
self.attached_files_layout.setContentsMargins(2, 2, 2, 2)
|
|
569
|
+
self.attached_files_layout.setSpacing(2)
|
|
551
570
|
self.attached_files_container.hide() # Initially hidden
|
|
552
571
|
input_layout.addWidget(self.attached_files_container)
|
|
553
572
|
layout.addWidget(self.input_container)
|
|
@@ -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
|
|
|
@@ -1109,6 +1140,7 @@ class QtChatBubble(QWidget):
|
|
|
1109
1140
|
|
|
1110
1141
|
if not self.attached_files:
|
|
1111
1142
|
self.attached_files_container.hide()
|
|
1143
|
+
self._adjust_window_size_for_attachments()
|
|
1112
1144
|
return
|
|
1113
1145
|
|
|
1114
1146
|
# Show container and add file chips
|
|
@@ -1124,14 +1156,14 @@ class QtChatBubble(QWidget):
|
|
|
1124
1156
|
QFrame {
|
|
1125
1157
|
background: rgba(0, 102, 204, 0.2);
|
|
1126
1158
|
border: 1px solid rgba(0, 102, 204, 0.4);
|
|
1127
|
-
border-radius:
|
|
1128
|
-
padding:
|
|
1159
|
+
border-radius: 6px;
|
|
1160
|
+
padding: 1px 4px;
|
|
1129
1161
|
}
|
|
1130
1162
|
""")
|
|
1131
1163
|
|
|
1132
1164
|
chip_layout = QHBoxLayout(file_chip)
|
|
1133
|
-
chip_layout.setContentsMargins(
|
|
1134
|
-
chip_layout.setSpacing(
|
|
1165
|
+
chip_layout.setContentsMargins(2, 1, 2, 1)
|
|
1166
|
+
chip_layout.setSpacing(2)
|
|
1135
1167
|
|
|
1136
1168
|
# File icon based on type
|
|
1137
1169
|
ext = os.path.splitext(file_name)[1].lower()
|
|
@@ -1151,18 +1183,18 @@ class QtChatBubble(QWidget):
|
|
|
1151
1183
|
icon = "📎"
|
|
1152
1184
|
|
|
1153
1185
|
file_label = QLabel(f"{icon} {file_name[:20]}{'...' if len(file_name) > 20 else ''}")
|
|
1154
|
-
file_label.setStyleSheet("background: transparent; border: none; color: rgba(255, 255, 255, 0.9); font-size:
|
|
1186
|
+
file_label.setStyleSheet("background: transparent; border: none; color: rgba(255, 255, 255, 0.9); font-size: 8px;")
|
|
1155
1187
|
chip_layout.addWidget(file_label)
|
|
1156
1188
|
|
|
1157
1189
|
# Remove button
|
|
1158
1190
|
remove_btn = QPushButton("✕")
|
|
1159
|
-
remove_btn.setFixedSize(
|
|
1191
|
+
remove_btn.setFixedSize(12, 12)
|
|
1160
1192
|
remove_btn.setStyleSheet("""
|
|
1161
1193
|
QPushButton {
|
|
1162
1194
|
background: transparent;
|
|
1163
1195
|
border: none;
|
|
1164
1196
|
color: rgba(255, 255, 255, 0.6);
|
|
1165
|
-
font-size:
|
|
1197
|
+
font-size: 8px;
|
|
1166
1198
|
padding: 0px;
|
|
1167
1199
|
}
|
|
1168
1200
|
QPushButton:hover {
|
|
@@ -1175,13 +1207,38 @@ class QtChatBubble(QWidget):
|
|
|
1175
1207
|
self.attached_files_layout.addWidget(file_chip)
|
|
1176
1208
|
|
|
1177
1209
|
self.attached_files_layout.addStretch()
|
|
1210
|
+
|
|
1211
|
+
# Adjust window size to accommodate file attachments
|
|
1212
|
+
self._adjust_window_size_for_attachments()
|
|
1213
|
+
|
|
1214
|
+
def _adjust_window_size_for_attachments(self):
|
|
1215
|
+
"""Dynamically adjust window size based on file attachments presence."""
|
|
1216
|
+
attachment_height = 28 # Height needed for file attachment container (reduced for compact chips)
|
|
1217
|
+
|
|
1218
|
+
if self.attached_files and self.attached_files_container.isVisible():
|
|
1219
|
+
# Files are attached - expand window
|
|
1220
|
+
new_height = self.base_height + attachment_height
|
|
1221
|
+
if self.debug:
|
|
1222
|
+
print(f"📏 Expanding window for attachments: {self.base_height} -> {new_height}")
|
|
1223
|
+
else:
|
|
1224
|
+
# No files attached - use base size
|
|
1225
|
+
new_height = self.base_height
|
|
1226
|
+
if self.debug:
|
|
1227
|
+
print(f"📏 Contracting window (no attachments): -> {new_height}")
|
|
1228
|
+
|
|
1229
|
+
# Apply new size
|
|
1230
|
+
self.setFixedSize(self.base_width, new_height)
|
|
1231
|
+
|
|
1232
|
+
# Reposition to maintain alignment with system tray
|
|
1233
|
+
self.position_near_tray()
|
|
1178
1234
|
|
|
1179
1235
|
def remove_attached_file(self, file_path):
|
|
1180
1236
|
"""Remove a file from the attached files list."""
|
|
1181
1237
|
if file_path in self.attached_files:
|
|
1182
1238
|
self.attached_files.remove(file_path)
|
|
1183
1239
|
if self.debug:
|
|
1184
|
-
|
|
1240
|
+
if self.debug:
|
|
1241
|
+
print(f"🗑️ Removed attached file: {file_path}")
|
|
1185
1242
|
self.update_attached_files_display()
|
|
1186
1243
|
|
|
1187
1244
|
def send_message(self):
|
|
@@ -1228,7 +1285,8 @@ class QtChatBubble(QWidget):
|
|
|
1228
1285
|
if self.status_callback:
|
|
1229
1286
|
self.status_callback("generating")
|
|
1230
1287
|
|
|
1231
|
-
|
|
1288
|
+
if self.debug:
|
|
1289
|
+
print("🔄 QtChatBubble: UI updated, creating worker thread...")
|
|
1232
1290
|
|
|
1233
1291
|
# 5. Start worker thread to send request with optional media files
|
|
1234
1292
|
self.worker = LLMWorker(
|
|
@@ -1241,17 +1299,20 @@ class QtChatBubble(QWidget):
|
|
|
1241
1299
|
self.worker.response_ready.connect(self.on_response_ready)
|
|
1242
1300
|
self.worker.error_occurred.connect(self.on_error_occurred)
|
|
1243
1301
|
|
|
1244
|
-
|
|
1302
|
+
if self.debug:
|
|
1303
|
+
print("🔄 QtChatBubble: Starting worker thread...")
|
|
1245
1304
|
self.worker.start()
|
|
1246
1305
|
|
|
1247
|
-
|
|
1306
|
+
if self.debug:
|
|
1307
|
+
print("🔄 QtChatBubble: Worker thread started, hiding bubble...")
|
|
1248
1308
|
# Hide bubble after sending (like the original design)
|
|
1249
1309
|
QTimer.singleShot(500, self.hide)
|
|
1250
1310
|
|
|
1251
1311
|
@pyqtSlot(str)
|
|
1252
1312
|
def on_response_ready(self, response):
|
|
1253
1313
|
"""Handle LLM response."""
|
|
1254
|
-
|
|
1314
|
+
if self.debug:
|
|
1315
|
+
print(f"✅ QtChatBubble: on_response_ready called with response: {response[:100]}...")
|
|
1255
1316
|
|
|
1256
1317
|
self.send_button.setEnabled(True)
|
|
1257
1318
|
self.send_button.setText("→")
|
|
@@ -1271,6 +1332,10 @@ class QtChatBubble(QWidget):
|
|
|
1271
1332
|
}
|
|
1272
1333
|
""")
|
|
1273
1334
|
|
|
1335
|
+
# Notify main app about status change (for icon animation)
|
|
1336
|
+
if self.status_callback:
|
|
1337
|
+
self.status_callback("ready")
|
|
1338
|
+
|
|
1274
1339
|
# Get updated message history from AbstractCore session
|
|
1275
1340
|
self._update_message_history_from_session()
|
|
1276
1341
|
|
|
@@ -1280,72 +1345,113 @@ class QtChatBubble(QWidget):
|
|
|
1280
1345
|
# Handle TTS if enabled (AbstractVoice integration)
|
|
1281
1346
|
if self.tts_enabled and self.voice_manager and self.voice_manager.is_available():
|
|
1282
1347
|
if self.debug:
|
|
1283
|
-
|
|
1348
|
+
if self.debug:
|
|
1349
|
+
print("🔊 TTS enabled, speaking response...")
|
|
1284
1350
|
|
|
1285
1351
|
# Don't show toast when TTS is enabled
|
|
1286
1352
|
try:
|
|
1287
1353
|
# Clean response for voice synthesis
|
|
1288
1354
|
clean_response = self._clean_response_for_voice(response)
|
|
1289
1355
|
|
|
1356
|
+
# Set up callbacks to detect when speech actually starts/ends
|
|
1357
|
+
# Use QMetaObject.invokeMethod to ensure callbacks run on main thread
|
|
1358
|
+
def on_speech_start():
|
|
1359
|
+
if self.debug:
|
|
1360
|
+
print("🔊 QtChatBubble: Speech actually started (background thread)")
|
|
1361
|
+
# Schedule status update on main thread
|
|
1362
|
+
QMetaObject.invokeMethod(self, "_on_speech_started_main_thread", Qt.QueuedConnection)
|
|
1363
|
+
|
|
1364
|
+
def on_speech_end():
|
|
1365
|
+
if self.debug:
|
|
1366
|
+
print("🔊 QtChatBubble: Speech ended (background thread)")
|
|
1367
|
+
# Schedule completion handling on main thread
|
|
1368
|
+
QMetaObject.invokeMethod(self, "_on_speech_ended_main_thread", Qt.QueuedConnection)
|
|
1369
|
+
|
|
1370
|
+
# Set the callbacks on the voice manager
|
|
1371
|
+
self.voice_manager.on_speech_start = on_speech_start
|
|
1372
|
+
self.voice_manager.on_speech_end = on_speech_end
|
|
1373
|
+
|
|
1290
1374
|
# Speak the cleaned response using AbstractVoice-compatible interface
|
|
1375
|
+
# Note: We don't set "speaking" status here anymore - we wait for the callback
|
|
1291
1376
|
self.voice_manager.speak(clean_response)
|
|
1292
1377
|
|
|
1293
1378
|
# Update toggle state to 'speaking'
|
|
1294
1379
|
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
1380
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1381
|
+
# Store response for callback when TTS completes
|
|
1382
|
+
self._pending_response = response
|
|
1307
1383
|
|
|
1308
1384
|
# Show chat history after TTS starts (small delay) - only if voice mode is OFF
|
|
1309
1385
|
QTimer.singleShot(800, self._show_history_if_voice_mode_off)
|
|
1310
1386
|
|
|
1311
1387
|
except Exception as e:
|
|
1312
1388
|
if self.debug:
|
|
1313
|
-
|
|
1389
|
+
if self.debug:
|
|
1390
|
+
print(f"❌ TTS error: {e}")
|
|
1314
1391
|
# Show chat history as fallback - only if voice mode is OFF
|
|
1315
1392
|
QTimer.singleShot(100, self._show_history_if_voice_mode_off)
|
|
1316
1393
|
else:
|
|
1317
1394
|
# Show chat history instead of toast when TTS is disabled - only if voice mode is OFF
|
|
1318
1395
|
self._show_history_if_voice_mode_off()
|
|
1319
1396
|
|
|
1320
|
-
#
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
self.
|
|
1397
|
+
# Handle status transitions based on TTS mode
|
|
1398
|
+
tts_will_handle = self.tts_enabled and self.voice_manager and self.voice_manager.is_available()
|
|
1399
|
+
if self.debug:
|
|
1400
|
+
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}")
|
|
1401
|
+
print(f"🔍 QtChatBubble: TTS will handle callbacks: {tts_will_handle}")
|
|
1402
|
+
|
|
1403
|
+
if not tts_will_handle:
|
|
1404
|
+
# Non-TTS path: Go directly to ready mode
|
|
1405
|
+
if self.debug:
|
|
1406
|
+
print(f"🔄 QtChatBubble: Non-TTS path - going to ready mode immediately")
|
|
1407
|
+
if self.response_callback:
|
|
1408
|
+
self.response_callback(response)
|
|
1409
|
+
if self.status_callback:
|
|
1410
|
+
self.status_callback("ready")
|
|
1411
|
+
else:
|
|
1412
|
+
# TTS path: Stay in thinking mode until audio actually starts
|
|
1413
|
+
if self.debug:
|
|
1414
|
+
print(f"🔊 QtChatBubble: TTS path - staying in thinking mode until audio starts")
|
|
1415
|
+
print(f"🔊 QtChatBubble: v0.5.1 callbacks will handle status transitions")
|
|
1416
|
+
# DON'T call response_callback or set "ready" status here!
|
|
1417
|
+
# The v0.5.1 callbacks will handle everything
|
|
1324
1418
|
|
|
1325
1419
|
def on_tts_toggled(self, enabled: bool):
|
|
1326
1420
|
"""Handle TTS toggle state change."""
|
|
1327
1421
|
self.tts_enabled = enabled
|
|
1328
1422
|
if self.debug:
|
|
1329
|
-
|
|
1423
|
+
if self.debug:
|
|
1424
|
+
print(f"🔊 TTS {'enabled' if enabled else 'disabled'}")
|
|
1330
1425
|
|
|
1331
1426
|
# Stop any current speech when disabling
|
|
1332
1427
|
if not enabled and self.voice_manager:
|
|
1333
1428
|
try:
|
|
1334
1429
|
self.voice_manager.stop()
|
|
1335
1430
|
self._update_tts_toggle_state()
|
|
1431
|
+
|
|
1432
|
+
# Manually trigger status update to "ready" since v0.5.1 callback won't fire
|
|
1433
|
+
# when we manually stop the audio
|
|
1434
|
+
if self.status_callback:
|
|
1435
|
+
if self.debug:
|
|
1436
|
+
print("🔊 QtChatBubble: TTS disabled, setting ready status")
|
|
1437
|
+
self.status_callback("ready")
|
|
1438
|
+
|
|
1336
1439
|
except Exception as e:
|
|
1337
1440
|
if self.debug:
|
|
1338
|
-
|
|
1441
|
+
if self.debug:
|
|
1442
|
+
print(f"❌ Error stopping TTS: {e}")
|
|
1339
1443
|
|
|
1340
|
-
# Update LLM session
|
|
1444
|
+
# Update LLM session mode while preserving chat history
|
|
1341
1445
|
if self.llm_manager:
|
|
1342
1446
|
try:
|
|
1343
|
-
self.llm_manager.
|
|
1447
|
+
self.llm_manager.update_session_mode(tts_mode=enabled)
|
|
1344
1448
|
if self.debug:
|
|
1345
|
-
|
|
1449
|
+
if self.debug:
|
|
1450
|
+
print(f"🔄 LLM session mode updated for {'TTS' if enabled else 'normal'} mode (history preserved)")
|
|
1346
1451
|
except Exception as e:
|
|
1347
1452
|
if self.debug:
|
|
1348
|
-
|
|
1453
|
+
if self.debug:
|
|
1454
|
+
print(f"❌ Error updating LLM session: {e}")
|
|
1349
1455
|
|
|
1350
1456
|
def on_tts_single_click(self):
|
|
1351
1457
|
"""Handle single click on TTS toggle - pause/resume functionality."""
|
|
@@ -1359,27 +1465,33 @@ class QtChatBubble(QWidget):
|
|
|
1359
1465
|
# Pause the speech - may need multiple attempts if audio stream just started
|
|
1360
1466
|
success = self._attempt_pause_with_retry()
|
|
1361
1467
|
if success and self.debug:
|
|
1362
|
-
|
|
1468
|
+
if self.debug:
|
|
1469
|
+
print("🔊 TTS paused via single click")
|
|
1363
1470
|
elif self.debug:
|
|
1364
|
-
|
|
1471
|
+
if self.debug:
|
|
1472
|
+
print("🔊 TTS pause failed - audio stream may not be ready yet")
|
|
1365
1473
|
elif current_state == 'paused':
|
|
1366
1474
|
# Resume the speech
|
|
1367
1475
|
success = self.voice_manager.resume()
|
|
1368
1476
|
if success and self.debug:
|
|
1369
|
-
|
|
1477
|
+
if self.debug:
|
|
1478
|
+
print("🔊 TTS resumed via single click")
|
|
1370
1479
|
elif self.debug:
|
|
1371
|
-
|
|
1480
|
+
if self.debug:
|
|
1481
|
+
print("🔊 TTS resume failed")
|
|
1372
1482
|
else:
|
|
1373
1483
|
# If idle, do nothing or could show a message
|
|
1374
1484
|
if self.debug:
|
|
1375
|
-
|
|
1485
|
+
if self.debug:
|
|
1486
|
+
print("🔊 TTS single click - no active speech to pause/resume")
|
|
1376
1487
|
|
|
1377
1488
|
# Update visual state
|
|
1378
1489
|
self._update_tts_toggle_state()
|
|
1379
1490
|
|
|
1380
1491
|
except Exception as e:
|
|
1381
1492
|
if self.debug:
|
|
1382
|
-
|
|
1493
|
+
if self.debug:
|
|
1494
|
+
print(f"❌ Error handling TTS single click: {e}")
|
|
1383
1495
|
|
|
1384
1496
|
def _attempt_pause_with_retry(self, max_attempts=5):
|
|
1385
1497
|
"""Attempt to pause with retry logic for timing issues.
|
|
@@ -1402,7 +1514,8 @@ class QtChatBubble(QWidget):
|
|
|
1402
1514
|
return True
|
|
1403
1515
|
|
|
1404
1516
|
if self.debug:
|
|
1405
|
-
|
|
1517
|
+
if self.debug:
|
|
1518
|
+
print(f"🔊 Pause attempt {attempt + 1}/{max_attempts} failed, retrying...")
|
|
1406
1519
|
|
|
1407
1520
|
# Short delay before retry
|
|
1408
1521
|
time.sleep(0.1)
|
|
@@ -1412,7 +1525,8 @@ class QtChatBubble(QWidget):
|
|
|
1412
1525
|
def on_tts_double_click(self):
|
|
1413
1526
|
"""Handle double click on TTS toggle - stop TTS and open chat bubble."""
|
|
1414
1527
|
if self.debug:
|
|
1415
|
-
|
|
1528
|
+
if self.debug:
|
|
1529
|
+
print("🔊 TTS double click - stopping speech and showing chat")
|
|
1416
1530
|
|
|
1417
1531
|
# Prevent double-free errors by checking if objects are still valid
|
|
1418
1532
|
try:
|
|
@@ -1426,10 +1540,18 @@ class QtChatBubble(QWidget):
|
|
|
1426
1540
|
# Safely update TTS toggle state
|
|
1427
1541
|
if hasattr(self, '_update_tts_toggle_state'):
|
|
1428
1542
|
self._update_tts_toggle_state()
|
|
1543
|
+
|
|
1544
|
+
# Manually trigger status update to "ready" since v0.5.1 callback won't fire
|
|
1545
|
+
# when we manually stop the audio
|
|
1546
|
+
if hasattr(self, 'status_callback') and self.status_callback:
|
|
1547
|
+
if self.debug:
|
|
1548
|
+
print("🔊 QtChatBubble: Manually stopped TTS, setting ready status")
|
|
1549
|
+
self.status_callback("ready")
|
|
1429
1550
|
|
|
1430
1551
|
except Exception as e:
|
|
1431
1552
|
if self.debug:
|
|
1432
|
-
|
|
1553
|
+
if self.debug:
|
|
1554
|
+
print(f"❌ Error stopping TTS on double click: {e}")
|
|
1433
1555
|
|
|
1434
1556
|
# Show the chat bubble with safety checks
|
|
1435
1557
|
if hasattr(self, 'show') and not self.isVisible():
|
|
@@ -1463,12 +1585,14 @@ class QtChatBubble(QWidget):
|
|
|
1463
1585
|
try:
|
|
1464
1586
|
# Ensure voice manager is available
|
|
1465
1587
|
if not self.voice_manager or not self.voice_manager.is_available():
|
|
1466
|
-
|
|
1588
|
+
if self.debug:
|
|
1589
|
+
print("❌ Voice manager not available for Full Voice Mode")
|
|
1467
1590
|
self.full_voice_toggle.set_enabled(False)
|
|
1468
1591
|
return
|
|
1469
1592
|
|
|
1470
1593
|
if self.debug:
|
|
1471
|
-
|
|
1594
|
+
if self.debug:
|
|
1595
|
+
print("🚀 Starting Full Voice Mode...")
|
|
1472
1596
|
|
|
1473
1597
|
# Hide text input UI
|
|
1474
1598
|
self.hide_text_ui()
|
|
@@ -1480,9 +1604,9 @@ class QtChatBubble(QWidget):
|
|
|
1480
1604
|
# Set up voice mode based on CLI parameter
|
|
1481
1605
|
self.voice_manager.set_voice_mode(self.listening_mode)
|
|
1482
1606
|
|
|
1483
|
-
# Update LLM session for voice-optimized responses
|
|
1607
|
+
# Update LLM session mode for voice-optimized responses (preserve history)
|
|
1484
1608
|
if self.llm_manager:
|
|
1485
|
-
self.llm_manager.
|
|
1609
|
+
self.llm_manager.update_session_mode(tts_mode=True)
|
|
1486
1610
|
|
|
1487
1611
|
# Start listening
|
|
1488
1612
|
self.voice_manager.listen(
|
|
@@ -1497,11 +1621,13 @@ class QtChatBubble(QWidget):
|
|
|
1497
1621
|
self.voice_manager.speak("Full voice mode activated. I'm listening...")
|
|
1498
1622
|
|
|
1499
1623
|
if self.debug:
|
|
1500
|
-
|
|
1624
|
+
if self.debug:
|
|
1625
|
+
print("✅ Full Voice Mode started successfully")
|
|
1501
1626
|
|
|
1502
1627
|
except Exception as e:
|
|
1503
1628
|
if self.debug:
|
|
1504
|
-
|
|
1629
|
+
if self.debug:
|
|
1630
|
+
print(f"❌ Error starting Full Voice Mode: {e}")
|
|
1505
1631
|
import traceback
|
|
1506
1632
|
traceback.print_exc()
|
|
1507
1633
|
|
|
@@ -1513,7 +1639,8 @@ class QtChatBubble(QWidget):
|
|
|
1513
1639
|
"""Stop Full Voice Mode and return to normal text mode."""
|
|
1514
1640
|
try:
|
|
1515
1641
|
if self.debug:
|
|
1516
|
-
|
|
1642
|
+
if self.debug:
|
|
1643
|
+
print("🛑 Stopping Full Voice Mode...")
|
|
1517
1644
|
|
|
1518
1645
|
# Stop listening
|
|
1519
1646
|
if self.voice_manager:
|
|
@@ -1527,11 +1654,13 @@ class QtChatBubble(QWidget):
|
|
|
1527
1654
|
self.update_status("READY")
|
|
1528
1655
|
|
|
1529
1656
|
if self.debug:
|
|
1530
|
-
|
|
1657
|
+
if self.debug:
|
|
1658
|
+
print("✅ Full Voice Mode stopped")
|
|
1531
1659
|
|
|
1532
1660
|
except Exception as e:
|
|
1533
1661
|
if self.debug:
|
|
1534
|
-
|
|
1662
|
+
if self.debug:
|
|
1663
|
+
print(f"❌ Error stopping Full Voice Mode: {e}")
|
|
1535
1664
|
import traceback
|
|
1536
1665
|
traceback.print_exc()
|
|
1537
1666
|
|
|
@@ -1539,7 +1668,8 @@ class QtChatBubble(QWidget):
|
|
|
1539
1668
|
"""Handle speech-to-text input from the user."""
|
|
1540
1669
|
try:
|
|
1541
1670
|
if self.debug:
|
|
1542
|
-
|
|
1671
|
+
if self.debug:
|
|
1672
|
+
print(f"👤 Voice input: {transcribed_text}")
|
|
1543
1673
|
|
|
1544
1674
|
# No longer updating voice toggle appearance - it's a simple user control
|
|
1545
1675
|
self.update_status("PROCESSING")
|
|
@@ -1555,7 +1685,8 @@ class QtChatBubble(QWidget):
|
|
|
1555
1685
|
self._update_message_history_from_session()
|
|
1556
1686
|
|
|
1557
1687
|
if self.debug:
|
|
1558
|
-
|
|
1688
|
+
if self.debug:
|
|
1689
|
+
print(f"🤖 AI response: {response[:100]}...")
|
|
1559
1690
|
|
|
1560
1691
|
# Speak the response
|
|
1561
1692
|
self.voice_manager.speak(response)
|
|
@@ -1565,7 +1696,8 @@ class QtChatBubble(QWidget):
|
|
|
1565
1696
|
|
|
1566
1697
|
except Exception as e:
|
|
1567
1698
|
if self.debug:
|
|
1568
|
-
|
|
1699
|
+
if self.debug:
|
|
1700
|
+
print(f"❌ Error handling voice input: {e}")
|
|
1569
1701
|
import traceback
|
|
1570
1702
|
traceback.print_exc()
|
|
1571
1703
|
|
|
@@ -1575,7 +1707,8 @@ class QtChatBubble(QWidget):
|
|
|
1575
1707
|
def handle_voice_stop(self):
|
|
1576
1708
|
"""Handle when user says 'stop' to exit Full Voice Mode."""
|
|
1577
1709
|
if self.debug:
|
|
1578
|
-
|
|
1710
|
+
if self.debug:
|
|
1711
|
+
print("🛑 User said 'stop' - exiting Full Voice Mode")
|
|
1579
1712
|
|
|
1580
1713
|
# Disable Full Voice Mode
|
|
1581
1714
|
self.full_voice_toggle.set_enabled(False)
|
|
@@ -1587,7 +1720,10 @@ class QtChatBubble(QWidget):
|
|
|
1587
1720
|
self.input_container.hide()
|
|
1588
1721
|
|
|
1589
1722
|
# Update window size to be smaller but maintain wider width
|
|
1590
|
-
|
|
1723
|
+
voice_base_height = 120
|
|
1724
|
+
attachment_height = 28 if (self.attached_files and self.attached_files_container.isVisible()) else 0
|
|
1725
|
+
voice_height = voice_base_height + attachment_height
|
|
1726
|
+
self.setFixedSize(self.base_width, voice_height) # Dynamic height for voice mode
|
|
1591
1727
|
|
|
1592
1728
|
def show_text_ui(self):
|
|
1593
1729
|
"""Show the text input interface when exiting Full Voice Mode."""
|
|
@@ -1595,8 +1731,8 @@ class QtChatBubble(QWidget):
|
|
|
1595
1731
|
if hasattr(self, 'input_container'):
|
|
1596
1732
|
self.input_container.show()
|
|
1597
1733
|
|
|
1598
|
-
# Restore normal window size with wider width
|
|
1599
|
-
self.
|
|
1734
|
+
# Restore normal window size with wider width - use dynamic sizing
|
|
1735
|
+
self._adjust_window_size_for_attachments()
|
|
1600
1736
|
|
|
1601
1737
|
def update_status(self, status_text: str):
|
|
1602
1738
|
"""Update the status label with the given text."""
|
|
@@ -1623,7 +1759,7 @@ class QtChatBubble(QWidget):
|
|
|
1623
1759
|
font-size: 10px;
|
|
1624
1760
|
font-weight: 600;
|
|
1625
1761
|
color: #ffffff;
|
|
1626
|
-
font-family: "
|
|
1762
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1627
1763
|
}}
|
|
1628
1764
|
""")
|
|
1629
1765
|
|
|
@@ -1634,103 +1770,17 @@ class QtChatBubble(QWidget):
|
|
|
1634
1770
|
current_state = self.voice_manager.get_state()
|
|
1635
1771
|
# No longer updating tts_toggle appearance - it's a simple user control
|
|
1636
1772
|
|
|
1637
|
-
#
|
|
1638
|
-
if hasattr(self, 'voice_control_panel'):
|
|
1639
|
-
if current_state in ['speaking', 'paused']:
|
|
1640
|
-
self.voice_control_panel.show()
|
|
1641
|
-
self._update_voice_control_panel(current_state)
|
|
1642
|
-
else:
|
|
1643
|
-
self.voice_control_panel.hide()
|
|
1773
|
+
# Voice control panel removed - no longer needed
|
|
1644
1774
|
|
|
1645
1775
|
if self.debug:
|
|
1646
|
-
|
|
1776
|
+
if self.debug:
|
|
1777
|
+
print(f"🔊 TTS toggle state updated to: {current_state}")
|
|
1647
1778
|
except Exception as e:
|
|
1648
1779
|
if self.debug:
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
def create_voice_control_panel(self):
|
|
1652
|
-
"""Create a prominent voice control panel that appears when TTS is active."""
|
|
1653
|
-
panel = QWidget()
|
|
1654
|
-
layout = QHBoxLayout()
|
|
1655
|
-
layout.setContentsMargins(4, 2, 4, 2)
|
|
1656
|
-
layout.setSpacing(4)
|
|
1657
|
-
|
|
1658
|
-
# Pause/Resume button
|
|
1659
|
-
self.voice_pause_button = QPushButton("⏸")
|
|
1660
|
-
self.voice_pause_button.setFixedSize(24, 24)
|
|
1661
|
-
self.voice_pause_button.setToolTip("Pause/Resume TTS (Space)")
|
|
1662
|
-
self.voice_pause_button.clicked.connect(self.on_tts_single_click)
|
|
1663
|
-
self.voice_pause_button.setStyleSheet("""
|
|
1664
|
-
QPushButton {
|
|
1665
|
-
background: rgba(255, 255, 255, 0.1);
|
|
1666
|
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
1667
|
-
border-radius: 12px;
|
|
1668
|
-
font-size: 12px;
|
|
1669
|
-
color: rgba(255, 255, 255, 0.9);
|
|
1670
|
-
font-weight: bold;
|
|
1671
|
-
}
|
|
1672
|
-
QPushButton:hover {
|
|
1673
|
-
background: rgba(255, 255, 255, 0.2);
|
|
1674
|
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
1675
|
-
}
|
|
1676
|
-
QPushButton:pressed {
|
|
1677
|
-
background: rgba(255, 255, 255, 0.05);
|
|
1678
|
-
}
|
|
1679
|
-
""")
|
|
1680
|
-
layout.addWidget(self.voice_pause_button)
|
|
1681
|
-
|
|
1682
|
-
# Stop button
|
|
1683
|
-
self.voice_stop_button = QPushButton("⏹")
|
|
1684
|
-
self.voice_stop_button.setFixedSize(24, 24)
|
|
1685
|
-
self.voice_stop_button.setToolTip("Stop TTS (Escape)")
|
|
1686
|
-
self.voice_stop_button.clicked.connect(self.on_tts_double_click)
|
|
1687
|
-
self.voice_stop_button.setStyleSheet("""
|
|
1688
|
-
QPushButton {
|
|
1689
|
-
background: rgba(255, 100, 100, 0.1);
|
|
1690
|
-
border: 1px solid rgba(255, 100, 100, 0.3);
|
|
1691
|
-
border-radius: 12px;
|
|
1692
|
-
font-size: 12px;
|
|
1693
|
-
color: rgba(255, 200, 200, 0.9);
|
|
1694
|
-
font-weight: bold;
|
|
1695
|
-
}
|
|
1696
|
-
QPushButton:hover {
|
|
1697
|
-
background: rgba(255, 100, 100, 0.2);
|
|
1698
|
-
border: 1px solid rgba(255, 100, 100, 0.4);
|
|
1699
|
-
}
|
|
1700
|
-
QPushButton:pressed {
|
|
1701
|
-
background: rgba(255, 100, 100, 0.05);
|
|
1702
|
-
}
|
|
1703
|
-
""")
|
|
1704
|
-
layout.addWidget(self.voice_stop_button)
|
|
1705
|
-
|
|
1706
|
-
# Status text
|
|
1707
|
-
self.voice_status_label = QLabel("Speaking...")
|
|
1708
|
-
self.voice_status_label.setStyleSheet("""
|
|
1709
|
-
QLabel {
|
|
1710
|
-
color: rgba(255, 255, 255, 0.8);
|
|
1711
|
-
font-size: 10px;
|
|
1712
|
-
font-weight: 500;
|
|
1713
|
-
padding: 2px 4px;
|
|
1714
|
-
}
|
|
1715
|
-
""")
|
|
1716
|
-
layout.addWidget(self.voice_status_label)
|
|
1717
|
-
|
|
1718
|
-
panel.setLayout(layout)
|
|
1719
|
-
return panel
|
|
1720
|
-
|
|
1721
|
-
def _update_voice_control_panel(self, state):
|
|
1722
|
-
"""Update the voice control panel based on TTS state."""
|
|
1723
|
-
if not hasattr(self, 'voice_control_panel'):
|
|
1724
|
-
return
|
|
1780
|
+
if self.debug:
|
|
1781
|
+
print(f"❌ Error updating TTS toggle state: {e}")
|
|
1725
1782
|
|
|
1726
|
-
|
|
1727
|
-
self.voice_pause_button.setText("⏸")
|
|
1728
|
-
self.voice_pause_button.setToolTip("Pause TTS (Space)")
|
|
1729
|
-
self.voice_status_label.setText("Speaking...")
|
|
1730
|
-
elif state == 'paused':
|
|
1731
|
-
self.voice_pause_button.setText("▶")
|
|
1732
|
-
self.voice_pause_button.setToolTip("Resume TTS (Space)")
|
|
1733
|
-
self.voice_status_label.setText("Paused")
|
|
1783
|
+
# Voice control panel methods removed - not needed
|
|
1734
1784
|
|
|
1735
1785
|
def setup_keyboard_shortcuts(self):
|
|
1736
1786
|
"""Setup keyboard shortcuts for voice control."""
|
|
@@ -1747,11 +1797,13 @@ class QtChatBubble(QWidget):
|
|
|
1747
1797
|
self.escape_shortcut.activated.connect(self.handle_escape_shortcut)
|
|
1748
1798
|
|
|
1749
1799
|
if self.debug:
|
|
1750
|
-
|
|
1800
|
+
if self.debug:
|
|
1801
|
+
print("✅ Keyboard shortcuts setup: Space (pause/resume), Escape (stop)")
|
|
1751
1802
|
|
|
1752
1803
|
except Exception as e:
|
|
1753
1804
|
if self.debug:
|
|
1754
|
-
|
|
1805
|
+
if self.debug:
|
|
1806
|
+
print(f"❌ Error setting up keyboard shortcuts: {e}")
|
|
1755
1807
|
|
|
1756
1808
|
def handle_space_shortcut(self):
|
|
1757
1809
|
"""Handle space bar shortcut for pause/resume."""
|
|
@@ -1760,14 +1812,16 @@ class QtChatBubble(QWidget):
|
|
|
1760
1812
|
not self.input_text.hasFocus()):
|
|
1761
1813
|
self.on_tts_single_click()
|
|
1762
1814
|
if self.debug:
|
|
1763
|
-
|
|
1815
|
+
if self.debug:
|
|
1816
|
+
print("🔊 Space shortcut triggered pause/resume")
|
|
1764
1817
|
|
|
1765
1818
|
def handle_escape_shortcut(self):
|
|
1766
1819
|
"""Handle escape key shortcut for stop."""
|
|
1767
1820
|
if self.voice_manager and self.voice_manager.get_state() in ['speaking', 'paused']:
|
|
1768
1821
|
self.on_tts_double_click()
|
|
1769
1822
|
if self.debug:
|
|
1770
|
-
|
|
1823
|
+
if self.debug:
|
|
1824
|
+
print("🔊 Escape shortcut triggered stop")
|
|
1771
1825
|
|
|
1772
1826
|
def _clean_response_for_voice(self, text: str) -> str:
|
|
1773
1827
|
"""Clean response text for voice synthesis - remove formatting and make conversational."""
|
|
@@ -1817,7 +1871,8 @@ class QtChatBubble(QWidget):
|
|
|
1817
1871
|
# NO TRUNCATION - let the LLM decide response length based on system prompt
|
|
1818
1872
|
|
|
1819
1873
|
if self.debug:
|
|
1820
|
-
|
|
1874
|
+
if self.debug:
|
|
1875
|
+
print(f"🔊 Cleaned text for TTS: {text[:100]}{'...' if len(text) > 100 else ''}")
|
|
1821
1876
|
|
|
1822
1877
|
return text
|
|
1823
1878
|
|
|
@@ -1843,11 +1898,13 @@ class QtChatBubble(QWidget):
|
|
|
1843
1898
|
""")
|
|
1844
1899
|
|
|
1845
1900
|
if self.debug:
|
|
1846
|
-
|
|
1901
|
+
if self.debug:
|
|
1902
|
+
print(f"Error occurred: {error}")
|
|
1847
1903
|
|
|
1848
1904
|
# Show chat history instead of error toast
|
|
1849
1905
|
if self.debug:
|
|
1850
|
-
|
|
1906
|
+
if self.debug:
|
|
1907
|
+
print(f"❌ AI Error: {error}")
|
|
1851
1908
|
|
|
1852
1909
|
# Show history so user can see the error context - only if voice mode is OFF
|
|
1853
1910
|
QTimer.singleShot(100, self._show_history_if_voice_mode_off)
|
|
@@ -1873,7 +1930,7 @@ class QtChatBubble(QWidget):
|
|
|
1873
1930
|
reply = QMessageBox.question(
|
|
1874
1931
|
self,
|
|
1875
1932
|
"Clear Session",
|
|
1876
|
-
"Are you sure you want to clear the current session?\nThis will remove all messages and reset the token count.",
|
|
1933
|
+
"Are you sure you want to clear the current session?\nThis will remove all messages, attached files, and reset the token count.",
|
|
1877
1934
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
1878
1935
|
QMessageBox.StandardButton.No
|
|
1879
1936
|
)
|
|
@@ -1887,14 +1944,20 @@ class QtChatBubble(QWidget):
|
|
|
1887
1944
|
if self.llm_manager:
|
|
1888
1945
|
self.llm_manager.create_new_session()
|
|
1889
1946
|
if self.debug:
|
|
1890
|
-
|
|
1947
|
+
if self.debug:
|
|
1948
|
+
print("🧹 AbstractCore session cleared and recreated")
|
|
1891
1949
|
|
|
1892
1950
|
self.message_history.clear()
|
|
1893
1951
|
self.token_count = 0
|
|
1894
1952
|
self.update_token_display()
|
|
1895
1953
|
|
|
1954
|
+
# Clear attached files as part of session clearing
|
|
1955
|
+
self.attached_files.clear()
|
|
1956
|
+
self.update_attached_files_display()
|
|
1957
|
+
|
|
1896
1958
|
if self.debug:
|
|
1897
|
-
|
|
1959
|
+
if self.debug:
|
|
1960
|
+
print("🧹 Session cleared (including attached files)")
|
|
1898
1961
|
|
|
1899
1962
|
def load_session(self):
|
|
1900
1963
|
"""Load a session using AbstractCore via LLMManager."""
|
|
@@ -1931,7 +1994,8 @@ class QtChatBubble(QWidget):
|
|
|
1931
1994
|
)
|
|
1932
1995
|
|
|
1933
1996
|
if self.debug:
|
|
1934
|
-
|
|
1997
|
+
if self.debug:
|
|
1998
|
+
print(f"📂 Loaded session via AbstractCore from {file_path}")
|
|
1935
1999
|
else:
|
|
1936
2000
|
raise Exception("Session loaded but not available in LLMManager")
|
|
1937
2001
|
else:
|
|
@@ -1944,7 +2008,8 @@ class QtChatBubble(QWidget):
|
|
|
1944
2008
|
f"Failed to load session via AbstractCore:\n{str(e)}"
|
|
1945
2009
|
)
|
|
1946
2010
|
if self.debug:
|
|
1947
|
-
|
|
2011
|
+
if self.debug:
|
|
2012
|
+
print(f"❌ Failed to load session: {e}")
|
|
1948
2013
|
|
|
1949
2014
|
def save_session(self):
|
|
1950
2015
|
"""Save the current session using AbstractCore via LLMManager."""
|
|
@@ -1980,7 +2045,8 @@ class QtChatBubble(QWidget):
|
|
|
1980
2045
|
)
|
|
1981
2046
|
|
|
1982
2047
|
if self.debug:
|
|
1983
|
-
|
|
2048
|
+
if self.debug:
|
|
2049
|
+
print(f"💾 Saved session via AbstractCore to {file_path}")
|
|
1984
2050
|
else:
|
|
1985
2051
|
raise Exception("AbstractCore session saving failed")
|
|
1986
2052
|
|
|
@@ -1991,7 +2057,8 @@ class QtChatBubble(QWidget):
|
|
|
1991
2057
|
f"Failed to save session via AbstractCore:\n{str(e)}"
|
|
1992
2058
|
)
|
|
1993
2059
|
if self.debug:
|
|
1994
|
-
|
|
2060
|
+
if self.debug:
|
|
2061
|
+
print(f"❌ Failed to save session: {e}")
|
|
1995
2062
|
|
|
1996
2063
|
def _is_voice_mode_active(self):
|
|
1997
2064
|
"""Centralized source of truth: Check if ANY voice mode is active."""
|
|
@@ -2038,11 +2105,34 @@ class QtChatBubble(QWidget):
|
|
|
2038
2105
|
self.message_history.append(message)
|
|
2039
2106
|
|
|
2040
2107
|
if self.debug:
|
|
2041
|
-
|
|
2108
|
+
if self.debug:
|
|
2109
|
+
print(f"📚 Updated message history from AbstractCore: {len(self.message_history)} messages")
|
|
2042
2110
|
|
|
2043
2111
|
except Exception as e:
|
|
2044
2112
|
if self.debug:
|
|
2045
|
-
|
|
2113
|
+
if self.debug:
|
|
2114
|
+
print(f"❌ Error updating message history from session: {e}")
|
|
2115
|
+
|
|
2116
|
+
def _rebuild_chat_display(self):
|
|
2117
|
+
"""Rebuild chat display after session loading.
|
|
2118
|
+
|
|
2119
|
+
Since the main bubble doesn't have a chat display area, this method
|
|
2120
|
+
updates the history dialog if it's currently open.
|
|
2121
|
+
"""
|
|
2122
|
+
try:
|
|
2123
|
+
# If history dialog is open, refresh it with new message history
|
|
2124
|
+
if self.history_dialog and self.history_dialog.isVisible():
|
|
2125
|
+
self.history_dialog.refresh_messages(self.message_history)
|
|
2126
|
+
if self.debug:
|
|
2127
|
+
print("🔄 Refreshed history dialog with loaded session messages")
|
|
2128
|
+
|
|
2129
|
+
# No action needed if history dialog is closed since main bubble has no chat display
|
|
2130
|
+
if self.debug:
|
|
2131
|
+
print("✅ Chat display rebuild completed")
|
|
2132
|
+
|
|
2133
|
+
except Exception as e:
|
|
2134
|
+
if self.debug:
|
|
2135
|
+
print(f"❌ Error rebuilding chat display: {e}")
|
|
2046
2136
|
|
|
2047
2137
|
def _update_token_count_from_session(self):
|
|
2048
2138
|
"""Update token count from AbstractCore session."""
|
|
@@ -2053,10 +2143,12 @@ class QtChatBubble(QWidget):
|
|
|
2053
2143
|
self.update_token_display()
|
|
2054
2144
|
|
|
2055
2145
|
if self.debug:
|
|
2056
|
-
|
|
2146
|
+
if self.debug:
|
|
2147
|
+
print(f"📊 Updated token count from AbstractCore: {self.token_count}")
|
|
2057
2148
|
except Exception as e:
|
|
2058
2149
|
if self.debug:
|
|
2059
|
-
|
|
2150
|
+
if self.debug:
|
|
2151
|
+
print(f"❌ Error updating token count from session: {e}")
|
|
2060
2152
|
|
|
2061
2153
|
def _show_history_if_voice_mode_off(self):
|
|
2062
2154
|
"""Show chat history only if voice mode is OFF."""
|
|
@@ -2125,7 +2217,7 @@ class QtChatBubble(QWidget):
|
|
|
2125
2217
|
border-radius: 11px;
|
|
2126
2218
|
font-size: 10px;
|
|
2127
2219
|
color: #ffffff;
|
|
2128
|
-
font-family: "
|
|
2220
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
2129
2221
|
padding: 0 10px;
|
|
2130
2222
|
font-weight: 600;
|
|
2131
2223
|
}
|
|
@@ -2142,7 +2234,7 @@ class QtChatBubble(QWidget):
|
|
|
2142
2234
|
border-radius: 11px;
|
|
2143
2235
|
font-size: 10px;
|
|
2144
2236
|
color: rgba(255, 255, 255, 0.7);
|
|
2145
|
-
font-family: "
|
|
2237
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
2146
2238
|
padding: 0 10px;
|
|
2147
2239
|
}
|
|
2148
2240
|
QPushButton:hover {
|
|
@@ -2154,7 +2246,8 @@ class QtChatBubble(QWidget):
|
|
|
2154
2246
|
def close_app(self):
|
|
2155
2247
|
"""Close the entire application completely."""
|
|
2156
2248
|
if self.debug:
|
|
2157
|
-
|
|
2249
|
+
if self.debug:
|
|
2250
|
+
print("🔄 Close button clicked - shutting down application")
|
|
2158
2251
|
|
|
2159
2252
|
# Stop TTS if running
|
|
2160
2253
|
if hasattr(self, 'voice_manager') and self.voice_manager:
|
|
@@ -2170,16 +2263,19 @@ class QtChatBubble(QWidget):
|
|
|
2170
2263
|
# ALWAYS try to call the app quit callback first
|
|
2171
2264
|
if hasattr(self, 'app_quit_callback') and self.app_quit_callback:
|
|
2172
2265
|
if self.debug:
|
|
2173
|
-
|
|
2266
|
+
if self.debug:
|
|
2267
|
+
print("🔄 Calling app quit callback")
|
|
2174
2268
|
try:
|
|
2175
2269
|
self.app_quit_callback()
|
|
2176
2270
|
except Exception as e:
|
|
2177
2271
|
if self.debug:
|
|
2178
|
-
|
|
2272
|
+
if self.debug:
|
|
2273
|
+
print(f"❌ App callback failed: {e}")
|
|
2179
2274
|
|
|
2180
2275
|
# ALWAYS force quit as well to ensure the app terminates
|
|
2181
2276
|
if self.debug:
|
|
2182
|
-
|
|
2277
|
+
if self.debug:
|
|
2278
|
+
print("🔄 Force quitting application")
|
|
2183
2279
|
|
|
2184
2280
|
# Get the QApplication instance
|
|
2185
2281
|
app = QApplication.instance()
|
|
@@ -2193,7 +2289,8 @@ class QtChatBubble(QWidget):
|
|
|
2193
2289
|
import sys
|
|
2194
2290
|
import os
|
|
2195
2291
|
if self.debug:
|
|
2196
|
-
|
|
2292
|
+
if self.debug:
|
|
2293
|
+
print("🔄 Force exit with sys.exit and os._exit")
|
|
2197
2294
|
try:
|
|
2198
2295
|
sys.exit(0)
|
|
2199
2296
|
except:
|
|
@@ -2204,6 +2301,53 @@ class QtChatBubble(QWidget):
|
|
|
2204
2301
|
"""Set callback to properly quit the main application."""
|
|
2205
2302
|
self.app_quit_callback = callback
|
|
2206
2303
|
|
|
2304
|
+
@pyqtSlot()
|
|
2305
|
+
def _on_speech_started_main_thread(self):
|
|
2306
|
+
"""Handle speech start on main thread (called via QMetaObject.invokeMethod)."""
|
|
2307
|
+
if self.debug:
|
|
2308
|
+
print("🔊 QtChatBubble: Speech started - updating status on main thread")
|
|
2309
|
+
if self.status_callback:
|
|
2310
|
+
self.status_callback("speaking")
|
|
2311
|
+
|
|
2312
|
+
@pyqtSlot()
|
|
2313
|
+
def _on_speech_ended_main_thread(self):
|
|
2314
|
+
"""Handle speech end on main thread (called via QMetaObject.invokeMethod)."""
|
|
2315
|
+
if self.debug:
|
|
2316
|
+
print("🔊 QtChatBubble: Speech ended - handling completion on main thread")
|
|
2317
|
+
|
|
2318
|
+
# Update toggle state when speech completes
|
|
2319
|
+
self._update_tts_toggle_state()
|
|
2320
|
+
|
|
2321
|
+
# Call response callback now that TTS is done
|
|
2322
|
+
if self.response_callback and hasattr(self, '_pending_response'):
|
|
2323
|
+
if self.debug:
|
|
2324
|
+
print(f"🔄 QtChatBubble: TTS completed, calling response callback...")
|
|
2325
|
+
self.response_callback(self._pending_response)
|
|
2326
|
+
delattr(self, '_pending_response')
|
|
2327
|
+
|
|
2328
|
+
# Notify main app that speaking is done (back to ready)
|
|
2329
|
+
if self.status_callback:
|
|
2330
|
+
if self.debug:
|
|
2331
|
+
print("🔊 QtChatBubble: Speech ended, setting ready status")
|
|
2332
|
+
self.status_callback("ready")
|
|
2333
|
+
|
|
2334
|
+
@pyqtSlot()
|
|
2335
|
+
def _execute_tts_completion_callbacks(self):
|
|
2336
|
+
"""Execute TTS completion callbacks on the main thread."""
|
|
2337
|
+
if hasattr(self, '_tts_completion_callback') and self._tts_completion_callback:
|
|
2338
|
+
if self.debug:
|
|
2339
|
+
print("🔊 QtChatBubble: Executing TTS completion callbacks on main thread...")
|
|
2340
|
+
|
|
2341
|
+
# Execute the stored callback
|
|
2342
|
+
try:
|
|
2343
|
+
self._tts_completion_callback()
|
|
2344
|
+
except Exception as e:
|
|
2345
|
+
if self.debug:
|
|
2346
|
+
print(f"❌ Error executing TTS completion callback: {e}")
|
|
2347
|
+
finally:
|
|
2348
|
+
# Clear the callback
|
|
2349
|
+
self._tts_completion_callback = None
|
|
2350
|
+
|
|
2207
2351
|
|
|
2208
2352
|
def closeEvent(self, event):
|
|
2209
2353
|
"""Handle close event."""
|
|
@@ -2217,7 +2361,8 @@ class QtChatBubble(QWidget):
|
|
|
2217
2361
|
self.voice_manager.cleanup()
|
|
2218
2362
|
except Exception as e:
|
|
2219
2363
|
if self.debug:
|
|
2220
|
-
|
|
2364
|
+
if self.debug:
|
|
2365
|
+
print(f"❌ Error cleaning up voice manager: {e}")
|
|
2221
2366
|
|
|
2222
2367
|
event.accept()
|
|
2223
2368
|
|
|
@@ -2241,20 +2386,21 @@ class QtBubbleManager:
|
|
|
2241
2386
|
raise RuntimeError("No Qt library available. Install PyQt5, PySide2, or PyQt6")
|
|
2242
2387
|
|
|
2243
2388
|
if self.debug:
|
|
2244
|
-
|
|
2389
|
+
if self.debug:
|
|
2390
|
+
print(f"✅ QtBubbleManager initialized with {QT_AVAILABLE}")
|
|
2245
2391
|
|
|
2246
2392
|
def _prepare_bubble(self):
|
|
2247
2393
|
"""Pre-initialize the bubble for instant display later."""
|
|
2248
2394
|
if not self.app:
|
|
2249
|
-
#
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
self.app = QApplication.instance()
|
|
2395
|
+
# Always use existing QApplication instance (never create a new one)
|
|
2396
|
+
self.app = QApplication.instance()
|
|
2397
|
+
if not self.app:
|
|
2398
|
+
raise RuntimeError("No QApplication instance found. This should be created by the main app first.")
|
|
2254
2399
|
|
|
2255
2400
|
if not self.bubble:
|
|
2256
2401
|
if self.debug:
|
|
2257
|
-
|
|
2402
|
+
if self.debug:
|
|
2403
|
+
print("🔄 Pre-creating QtChatBubble...")
|
|
2258
2404
|
|
|
2259
2405
|
# Create the bubble but don't show it yet
|
|
2260
2406
|
self.bubble = QtChatBubble(self.llm_manager, self.config, self.debug, self.listening_mode)
|
|
@@ -2268,7 +2414,8 @@ class QtBubbleManager:
|
|
|
2268
2414
|
self.bubble.set_status_callback(self.status_callback)
|
|
2269
2415
|
|
|
2270
2416
|
if self.debug:
|
|
2271
|
-
|
|
2417
|
+
if self.debug:
|
|
2418
|
+
print("✅ QtChatBubble pre-created and ready")
|
|
2272
2419
|
|
|
2273
2420
|
def show(self):
|
|
2274
2421
|
"""Show the chat bubble (instantly if pre-initialized)."""
|
|
@@ -2286,7 +2433,8 @@ class QtBubbleManager:
|
|
|
2286
2433
|
self.bubble.activateWindow()
|
|
2287
2434
|
|
|
2288
2435
|
if self.debug:
|
|
2289
|
-
|
|
2436
|
+
if self.debug:
|
|
2437
|
+
print("💬 Qt chat bubble shown")
|
|
2290
2438
|
|
|
2291
2439
|
def hide(self):
|
|
2292
2440
|
"""Hide the chat bubble."""
|
|
@@ -2294,7 +2442,8 @@ class QtBubbleManager:
|
|
|
2294
2442
|
self.bubble.hide()
|
|
2295
2443
|
|
|
2296
2444
|
if self.debug:
|
|
2297
|
-
|
|
2445
|
+
if self.debug:
|
|
2446
|
+
print("💬 Qt chat bubble hidden")
|
|
2298
2447
|
|
|
2299
2448
|
def destroy(self):
|
|
2300
2449
|
"""Destroy the chat bubble."""
|
|
@@ -2303,7 +2452,8 @@ class QtBubbleManager:
|
|
|
2303
2452
|
self.bubble = None
|
|
2304
2453
|
|
|
2305
2454
|
if self.debug:
|
|
2306
|
-
|
|
2455
|
+
if self.debug:
|
|
2456
|
+
print("💬 Qt chat bubble destroyed")
|
|
2307
2457
|
|
|
2308
2458
|
def set_response_callback(self, callback):
|
|
2309
2459
|
"""Set response callback."""
|