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
abstractassistant/app.py
CHANGED
|
@@ -33,7 +33,8 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
33
33
|
self.DOUBLE_CLICK_TIMEOUT = 300 # milliseconds
|
|
34
34
|
|
|
35
35
|
if self.debug:
|
|
36
|
-
|
|
36
|
+
if hasattr(self, 'debug') and self.debug:
|
|
37
|
+
print(f"🔄 EnhancedClickableIcon created with single_click: {single_click_handler is not None}, double_click: {double_click_handler is not None}")
|
|
37
38
|
|
|
38
39
|
# Create with no menu initially
|
|
39
40
|
super().__init__(name, image, text, menu=None)
|
|
@@ -42,7 +43,8 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
42
43
|
def _menu(self):
|
|
43
44
|
"""Override _menu property to intercept access and handle click timing."""
|
|
44
45
|
if self.debug:
|
|
45
|
-
|
|
46
|
+
if hasattr(self, 'debug') and self.debug:
|
|
47
|
+
print(f"🔍 _menu property accessed! Click count: {self.click_count}")
|
|
46
48
|
|
|
47
49
|
self._handle_click_timing()
|
|
48
50
|
# Return None so no menu is displayed
|
|
@@ -66,7 +68,8 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
66
68
|
self.click_timer.start()
|
|
67
69
|
|
|
68
70
|
if self.debug:
|
|
69
|
-
|
|
71
|
+
if hasattr(self, 'debug') and self.debug:
|
|
72
|
+
print("🔄 First click detected, starting timer...")
|
|
70
73
|
|
|
71
74
|
elif self.click_count == 2:
|
|
72
75
|
# Second click - cancel timer and execute double click
|
|
@@ -78,7 +81,8 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
78
81
|
self._execute_double_click()
|
|
79
82
|
|
|
80
83
|
if self.debug:
|
|
81
|
-
|
|
84
|
+
if hasattr(self, 'debug') and self.debug:
|
|
85
|
+
print("🔄 Double click detected!")
|
|
82
86
|
|
|
83
87
|
def _execute_single_click(self):
|
|
84
88
|
"""Execute single click handler after timeout."""
|
|
@@ -86,13 +90,15 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
86
90
|
self.click_timer = None
|
|
87
91
|
|
|
88
92
|
if self.debug:
|
|
89
|
-
|
|
93
|
+
if hasattr(self, 'debug') and self.debug:
|
|
94
|
+
print("✅ Single click detected on system tray icon!")
|
|
90
95
|
|
|
91
96
|
if self.single_click_handler:
|
|
92
97
|
try:
|
|
93
98
|
self.single_click_handler()
|
|
94
99
|
except Exception as e:
|
|
95
|
-
|
|
100
|
+
if hasattr(self, 'debug') and self.debug:
|
|
101
|
+
print(f"❌ Single click handler error: {e}")
|
|
96
102
|
if self.debug:
|
|
97
103
|
import traceback
|
|
98
104
|
traceback.print_exc()
|
|
@@ -100,13 +106,15 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
100
106
|
def _execute_double_click(self):
|
|
101
107
|
"""Execute double click handler immediately."""
|
|
102
108
|
if self.debug:
|
|
103
|
-
|
|
109
|
+
if hasattr(self, 'debug') and self.debug:
|
|
110
|
+
print("✅ Double click detected on system tray icon!")
|
|
104
111
|
|
|
105
112
|
if self.double_click_handler:
|
|
106
113
|
try:
|
|
107
114
|
self.double_click_handler()
|
|
108
115
|
except Exception as e:
|
|
109
|
-
|
|
116
|
+
if hasattr(self, 'debug') and self.debug:
|
|
117
|
+
print(f"❌ Double click handler error: {e}")
|
|
110
118
|
if self.debug:
|
|
111
119
|
import traceback
|
|
112
120
|
traceback.print_exc()
|
|
@@ -115,7 +123,8 @@ class EnhancedClickableIcon(pystray.Icon):
|
|
|
115
123
|
def _menu(self, value):
|
|
116
124
|
"""Allow setting _menu during initialization."""
|
|
117
125
|
if self.debug:
|
|
118
|
-
|
|
126
|
+
if hasattr(self, 'debug') and self.debug:
|
|
127
|
+
print(f"🔍 _menu property set to: {value}")
|
|
119
128
|
self._stored_menu = value
|
|
120
129
|
|
|
121
130
|
|
|
@@ -136,7 +145,8 @@ class AbstractAssistantApp:
|
|
|
136
145
|
|
|
137
146
|
# Validate configuration
|
|
138
147
|
if not self.config.validate():
|
|
139
|
-
|
|
148
|
+
if self.debug:
|
|
149
|
+
print("Warning: Configuration validation failed, using defaults")
|
|
140
150
|
self.config = Config.default()
|
|
141
151
|
|
|
142
152
|
# Initialize components
|
|
@@ -149,19 +159,31 @@ class AbstractAssistantApp:
|
|
|
149
159
|
self.is_running: bool = False
|
|
150
160
|
self.bubble_visible: bool = False
|
|
151
161
|
|
|
162
|
+
# Icon animation state
|
|
163
|
+
self.base_icon: Optional[Image.Image] = None
|
|
164
|
+
self.animation_timer: Optional[threading.Timer] = None
|
|
165
|
+
self.current_status: str = "ready"
|
|
166
|
+
|
|
152
167
|
if self.debug:
|
|
153
168
|
print(f"AbstractAssistant initialized with config: {self.config.to_dict()}")
|
|
154
169
|
|
|
155
170
|
def create_system_tray_icon(self) -> pystray.Icon:
|
|
156
171
|
"""Create and configure the system tray icon."""
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
# Try to use the app bundle icon first, fallback to generated icon
|
|
173
|
+
self.base_icon = self._load_app_bundle_icon()
|
|
174
|
+
if not self.base_icon:
|
|
175
|
+
# Generate a modern, clean icon - start with ready state (green, steady)
|
|
176
|
+
self.base_icon = self.icon_generator.create_app_icon(
|
|
177
|
+
color_scheme="green", # Ready state: steady green
|
|
178
|
+
animated=False # Ready state: no animation
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Apply initial heartbeat effect
|
|
182
|
+
icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, "ready")
|
|
162
183
|
|
|
163
184
|
if self.debug:
|
|
164
|
-
|
|
185
|
+
if self.debug:
|
|
186
|
+
print("🔄 Creating enhanced system tray icon with single/double click detection")
|
|
165
187
|
|
|
166
188
|
# Use our enhanced ClickableIcon for single/double click handling
|
|
167
189
|
return EnhancedClickableIcon(
|
|
@@ -173,138 +195,189 @@ class AbstractAssistantApp:
|
|
|
173
195
|
debug=self.debug
|
|
174
196
|
)
|
|
175
197
|
|
|
198
|
+
def _load_app_bundle_icon(self) -> Optional[Image.Image]:
|
|
199
|
+
"""Load the icon from the app bundle if available."""
|
|
200
|
+
try:
|
|
201
|
+
from pathlib import Path
|
|
202
|
+
# Try to find the app bundle icon
|
|
203
|
+
app_bundle_icon = Path("/Applications/AbstractAssistant.app/Contents/Resources/icon.png")
|
|
204
|
+
|
|
205
|
+
if self.debug:
|
|
206
|
+
if self.debug:
|
|
207
|
+
print(f"🔍 Looking for app bundle icon at: {app_bundle_icon}")
|
|
208
|
+
print(f" Exists: {app_bundle_icon.exists()}")
|
|
209
|
+
|
|
210
|
+
if app_bundle_icon.exists():
|
|
211
|
+
base_icon = Image.open(app_bundle_icon)
|
|
212
|
+
|
|
213
|
+
if self.debug:
|
|
214
|
+
if self.debug:
|
|
215
|
+
print(f"✅ Loaded app bundle icon: {base_icon.size} {base_icon.mode}")
|
|
216
|
+
|
|
217
|
+
# Resize to system tray size if needed
|
|
218
|
+
target_size = (self.config.system_tray.icon_size, self.config.system_tray.icon_size)
|
|
219
|
+
if base_icon.size != target_size:
|
|
220
|
+
if self.debug:
|
|
221
|
+
if self.debug:
|
|
222
|
+
print(f"🔄 Resizing from {base_icon.size} to {target_size}")
|
|
223
|
+
base_icon = base_icon.resize(target_size, Image.Resampling.LANCZOS)
|
|
224
|
+
|
|
225
|
+
return base_icon
|
|
226
|
+
except Exception as e:
|
|
227
|
+
if self.debug:
|
|
228
|
+
if self.debug:
|
|
229
|
+
print(f"❌ Could not load app bundle icon: {e}")
|
|
230
|
+
return None
|
|
231
|
+
|
|
176
232
|
def update_icon_status(self, status: str):
|
|
177
233
|
"""Update the system tray icon based on application status.
|
|
178
234
|
|
|
179
235
|
Args:
|
|
180
|
-
status: 'ready', 'generating', 'executing', 'thinking'
|
|
236
|
+
status: 'ready', 'generating', 'executing', 'thinking', 'speaking'
|
|
181
237
|
"""
|
|
182
|
-
if
|
|
238
|
+
if self.debug:
|
|
239
|
+
print(f"🔄 update_icon_status called with: {status}")
|
|
240
|
+
print(f" Previous status: {self.current_status}")
|
|
241
|
+
|
|
242
|
+
if not self.icon and not (hasattr(self, 'qt_tray_icon') and self.qt_tray_icon):
|
|
243
|
+
if self.debug:
|
|
244
|
+
print("⚠️ No icon available for status update")
|
|
183
245
|
return
|
|
184
|
-
|
|
246
|
+
|
|
247
|
+
if not self.base_icon:
|
|
248
|
+
if self.debug:
|
|
249
|
+
print("⚠️ No base icon available for status update")
|
|
250
|
+
return
|
|
251
|
+
|
|
185
252
|
try:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
253
|
+
# Stop any existing animation timer
|
|
254
|
+
self._stop_animation_timer()
|
|
255
|
+
|
|
256
|
+
# Map status to animation type
|
|
257
|
+
animation_status = status
|
|
258
|
+
if status in ["thinking", "generating", "executing"]:
|
|
259
|
+
animation_status = "thinking" # All working states use thinking animation
|
|
260
|
+
elif status == "speaking":
|
|
261
|
+
animation_status = "speaking"
|
|
194
262
|
else:
|
|
195
|
-
# Default
|
|
196
|
-
icon_image = self.icon_generator.create_app_icon(
|
|
197
|
-
color_scheme="green",
|
|
198
|
-
animated=False
|
|
199
|
-
)
|
|
263
|
+
animation_status = "ready" # Default to ready
|
|
200
264
|
|
|
201
|
-
# Update
|
|
202
|
-
self.
|
|
265
|
+
# Update current status AFTER determining animation type
|
|
266
|
+
self.current_status = animation_status
|
|
267
|
+
|
|
268
|
+
# Start appropriate animation
|
|
269
|
+
self._start_heartbeat_animation(animation_status)
|
|
203
270
|
|
|
204
271
|
if self.debug:
|
|
205
|
-
print(f"🎨 Updated icon status
|
|
272
|
+
print(f"🎨 Updated icon status: {status} -> animation: {animation_status}")
|
|
206
273
|
|
|
207
274
|
except Exception as e:
|
|
208
275
|
if self.debug:
|
|
209
276
|
print(f"❌ Error updating icon status: {e}")
|
|
210
277
|
|
|
211
|
-
def
|
|
212
|
-
"""Start
|
|
278
|
+
def _start_heartbeat_animation(self, status: str):
|
|
279
|
+
"""Start smooth heartbeat animation for the given status."""
|
|
280
|
+
if self.debug:
|
|
281
|
+
print(f"🎬 Starting smooth heartbeat animation for: {status}")
|
|
282
|
+
print(f" pystray icon available: {self.icon is not None}")
|
|
283
|
+
print(f" Qt icon available: {hasattr(self, 'qt_tray_icon') and self.qt_tray_icon is not None}")
|
|
284
|
+
print(f" Base icon available: {self.base_icon is not None}")
|
|
285
|
+
|
|
286
|
+
# Stop any existing animation
|
|
287
|
+
self._stop_animation_timer()
|
|
288
|
+
|
|
289
|
+
# Use Qt timer if we're in Qt mode, otherwise use threading timer
|
|
290
|
+
if hasattr(self, 'qt_tray_icon') and self.qt_tray_icon is not None:
|
|
291
|
+
self._start_qt_animation(status)
|
|
292
|
+
else:
|
|
293
|
+
self._start_threading_animation(status)
|
|
294
|
+
|
|
295
|
+
def _start_qt_animation(self, status: str):
|
|
296
|
+
"""Start Qt-based animation using QTimer."""
|
|
213
297
|
try:
|
|
214
|
-
import
|
|
215
|
-
import time
|
|
216
|
-
|
|
217
|
-
# Stop any existing timer
|
|
218
|
-
self._stop_working_animation()
|
|
219
|
-
|
|
220
|
-
# Create a heartbeat-like animation with dynamic timing
|
|
221
|
-
def update_working_icon():
|
|
222
|
-
if self.icon:
|
|
223
|
-
try:
|
|
224
|
-
icon_image = self.icon_generator.create_app_icon(
|
|
225
|
-
color_scheme="working",
|
|
226
|
-
animated=True
|
|
227
|
-
)
|
|
228
|
-
self.icon.icon = icon_image
|
|
229
|
-
except Exception as e:
|
|
230
|
-
if self.debug:
|
|
231
|
-
print(f"❌ Error updating working icon: {e}")
|
|
298
|
+
from PyQt5.QtCore import QTimer
|
|
232
299
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
300
|
+
def update_icon():
|
|
301
|
+
try:
|
|
302
|
+
if self.base_icon and self.current_status == status and hasattr(self, 'qt_tray_icon'):
|
|
303
|
+
# Apply smooth heartbeat effect
|
|
304
|
+
icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, status)
|
|
305
|
+
self._update_qt_icon(icon_image)
|
|
306
|
+
elif self.debug:
|
|
307
|
+
print(f"⚠️ Qt animation stopped - status_match:{self.current_status == status}")
|
|
308
|
+
if hasattr(self, 'qt_animation_timer'):
|
|
309
|
+
self.qt_animation_timer.stop()
|
|
310
|
+
except Exception as e:
|
|
311
|
+
if self.debug:
|
|
312
|
+
print(f"❌ Error in Qt animation: {e}")
|
|
243
313
|
|
|
244
|
-
|
|
245
|
-
self.
|
|
246
|
-
self.
|
|
314
|
+
# Create Qt timer for smooth animation
|
|
315
|
+
self.qt_animation_timer = QTimer()
|
|
316
|
+
self.qt_animation_timer.timeout.connect(update_icon)
|
|
317
|
+
self.qt_animation_timer.start(50) # 20 FPS (50ms intervals)
|
|
247
318
|
|
|
248
319
|
if self.debug:
|
|
249
|
-
print("
|
|
320
|
+
print("✅ Qt animation timer started")
|
|
250
321
|
|
|
251
322
|
except Exception as e:
|
|
252
323
|
if self.debug:
|
|
253
|
-
print(f"❌ Error starting
|
|
324
|
+
print(f"❌ Error starting Qt animation: {e}")
|
|
325
|
+
|
|
326
|
+
def _start_threading_animation(self, status: str):
|
|
327
|
+
"""Start threading-based animation using threading.Timer."""
|
|
328
|
+
def update_icon():
|
|
329
|
+
try:
|
|
330
|
+
if self.icon and self.base_icon and self.current_status == status:
|
|
331
|
+
# Apply smooth heartbeat effect
|
|
332
|
+
icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, status)
|
|
333
|
+
self.icon.icon = icon_image
|
|
334
|
+
|
|
335
|
+
# Schedule next update at 20 FPS for smooth animation
|
|
336
|
+
self.animation_timer = threading.Timer(0.05, update_icon)
|
|
337
|
+
self.animation_timer.start()
|
|
338
|
+
elif self.debug:
|
|
339
|
+
print(f"⚠️ Threading animation stopped - status_match:{self.current_status == status}")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
if self.debug:
|
|
342
|
+
print(f"❌ Error in threading animation: {e}")
|
|
343
|
+
|
|
344
|
+
# Start the threading animation
|
|
345
|
+
update_icon()
|
|
254
346
|
|
|
255
|
-
def
|
|
256
|
-
"""
|
|
347
|
+
def _update_qt_icon(self, icon_image):
|
|
348
|
+
"""Update Qt system tray icon with new image."""
|
|
257
349
|
try:
|
|
258
|
-
import
|
|
259
|
-
import
|
|
350
|
+
from PyQt5.QtGui import QIcon, QPixmap
|
|
351
|
+
import io
|
|
260
352
|
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
353
|
+
# Convert PIL image to QPixmap
|
|
354
|
+
img_buffer = io.BytesIO()
|
|
355
|
+
icon_image.save(img_buffer, format='PNG')
|
|
356
|
+
img_buffer.seek(0)
|
|
264
357
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
icon_image = self.icon_generator.create_app_icon(
|
|
269
|
-
color_scheme="green",
|
|
270
|
-
animated=True
|
|
271
|
-
)
|
|
272
|
-
self.icon.icon = icon_image
|
|
273
|
-
except Exception as e:
|
|
274
|
-
if self.debug:
|
|
275
|
-
print(f"❌ Error updating ready icon: {e}")
|
|
276
|
-
|
|
277
|
-
# Gentle heartbeat timer - slower, more subtle
|
|
278
|
-
def ready_timer_loop():
|
|
279
|
-
while hasattr(self, 'ready_active') and self.ready_active:
|
|
280
|
-
update_ready_icon()
|
|
281
|
-
time.sleep(0.1) # Update every 100ms for smooth animation
|
|
358
|
+
pixmap = QPixmap()
|
|
359
|
+
pixmap.loadFromData(img_buffer.getvalue())
|
|
360
|
+
qt_icon = QIcon(pixmap)
|
|
282
361
|
|
|
283
|
-
|
|
284
|
-
self.
|
|
285
|
-
self.ready_timer.start()
|
|
362
|
+
# Update the Qt tray icon
|
|
363
|
+
self.qt_tray_icon.setIcon(qt_icon)
|
|
286
364
|
|
|
287
|
-
if self.debug:
|
|
288
|
-
print("🎨 Started ready heartbeat animation")
|
|
289
|
-
|
|
290
365
|
except Exception as e:
|
|
291
366
|
if self.debug:
|
|
292
|
-
print(f"❌ Error
|
|
367
|
+
print(f"❌ Error updating Qt icon: {e}")
|
|
293
368
|
|
|
294
|
-
def
|
|
295
|
-
"""Stop the
|
|
296
|
-
if
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
369
|
+
def _stop_animation_timer(self):
|
|
370
|
+
"""Stop the current animation timer (both Qt and threading)."""
|
|
371
|
+
# Stop Qt timer if it exists
|
|
372
|
+
if hasattr(self, 'qt_animation_timer') and self.qt_animation_timer:
|
|
373
|
+
self.qt_animation_timer.stop()
|
|
374
|
+
self.qt_animation_timer = None
|
|
375
|
+
|
|
376
|
+
# Stop threading timer if it exists
|
|
377
|
+
if hasattr(self, 'animation_timer') and self.animation_timer:
|
|
378
|
+
self.animation_timer.cancel()
|
|
379
|
+
self.animation_timer = None
|
|
300
380
|
|
|
301
|
-
def _stop_working_animation(self):
|
|
302
|
-
"""Stop the working animation."""
|
|
303
|
-
if hasattr(self, 'working_active'):
|
|
304
|
-
self.working_active = False
|
|
305
|
-
self._stop_ready_animation() # Also stop ready animation
|
|
306
|
-
if self.debug:
|
|
307
|
-
print("🎨 Stopped working animation")
|
|
308
381
|
|
|
309
382
|
|
|
310
383
|
def show_chat_bubble(self, icon=None, item=None):
|
|
@@ -453,6 +526,11 @@ class AbstractAssistantApp:
|
|
|
453
526
|
voice_manager.stop()
|
|
454
527
|
if self.debug:
|
|
455
528
|
print("⏹ Voice stopped")
|
|
529
|
+
|
|
530
|
+
# Update icon status to ready since v0.5.1 callback won't fire for manual stops
|
|
531
|
+
self.update_icon_status("ready")
|
|
532
|
+
if self.debug:
|
|
533
|
+
print("🔄 Icon status set to ready after manual voice stop")
|
|
456
534
|
|
|
457
535
|
# Always show chat bubble on double click
|
|
458
536
|
if self.debug:
|
|
@@ -501,10 +579,12 @@ class AbstractAssistantApp:
|
|
|
501
579
|
def show_toast_notification(self, message: str, type: str = "info"):
|
|
502
580
|
"""Show a toast notification."""
|
|
503
581
|
icon = "✅" if type == "success" else "❌" if type == "error" else "ℹ️"
|
|
504
|
-
|
|
582
|
+
if self.debug:
|
|
583
|
+
print(f"{icon} {message}")
|
|
505
584
|
|
|
506
585
|
if self.debug:
|
|
507
|
-
|
|
586
|
+
if self.debug:
|
|
587
|
+
print(f"Toast notification: {type} - {message}")
|
|
508
588
|
|
|
509
589
|
# Show a proper macOS notification
|
|
510
590
|
try:
|
|
@@ -538,22 +618,40 @@ class AbstractAssistantApp:
|
|
|
538
618
|
"""Update application status."""
|
|
539
619
|
# Status is now handled by the web interface
|
|
540
620
|
if self.debug:
|
|
541
|
-
|
|
621
|
+
if self.debug:
|
|
622
|
+
print(f"Status update: {status}")
|
|
542
623
|
|
|
543
624
|
def clear_session(self, icon=None, item=None):
|
|
544
|
-
"""Clear the current session."""
|
|
625
|
+
"""Clear the current session with user confirmation."""
|
|
545
626
|
try:
|
|
546
627
|
if self.debug:
|
|
547
|
-
print("🔄
|
|
628
|
+
print("🔄 System tray clear session requested...")
|
|
548
629
|
|
|
549
|
-
|
|
630
|
+
# CRITICAL: System tray actions MUST have user confirmation
|
|
631
|
+
# Use the bubble's clear_session method which includes confirmation dialog
|
|
632
|
+
if hasattr(self, 'bubble_manager') and self.bubble_manager:
|
|
633
|
+
# Delegate to bubble manager which has proper user confirmation
|
|
634
|
+
bubble = self.bubble_manager.get_current_bubble()
|
|
635
|
+
if bubble:
|
|
636
|
+
bubble.clear_session() # This includes user confirmation dialog
|
|
637
|
+
return
|
|
550
638
|
|
|
639
|
+
# Fallback: Show notification that clearing requires UI interaction
|
|
640
|
+
try:
|
|
641
|
+
from .ui.toast_window import show_toast_notification
|
|
642
|
+
show_toast_notification(
|
|
643
|
+
"To clear session, please use the Clear button in the chat interface",
|
|
644
|
+
debug=self.debug
|
|
645
|
+
)
|
|
646
|
+
except:
|
|
647
|
+
print("💬 To clear session, please use the Clear button in the chat interface")
|
|
648
|
+
|
|
551
649
|
if self.debug:
|
|
552
|
-
print("
|
|
650
|
+
print("⚠️ System tray clear session requires user confirmation via UI")
|
|
553
651
|
|
|
554
652
|
except Exception as e:
|
|
555
653
|
if self.debug:
|
|
556
|
-
print(f"❌ Error
|
|
654
|
+
print(f"❌ Error in clear session request: {e}")
|
|
557
655
|
|
|
558
656
|
def save_session(self, icon=None, item=None):
|
|
559
657
|
"""Save the current session to file."""
|
|
@@ -593,58 +691,36 @@ class AbstractAssistantApp:
|
|
|
593
691
|
print(f"❌ Error saving session: {e}")
|
|
594
692
|
|
|
595
693
|
def load_session(self, icon=None, item=None):
|
|
596
|
-
"""Load a session from file."""
|
|
694
|
+
"""Load a session from file with user confirmation."""
|
|
597
695
|
try:
|
|
598
696
|
if self.debug:
|
|
599
|
-
print("🔄
|
|
600
|
-
|
|
601
|
-
# Get sessions directory
|
|
602
|
-
import os
|
|
603
|
-
sessions_dir = os.path.join(os.path.expanduser("~"), ".abstractassistant", "sessions")
|
|
604
|
-
|
|
605
|
-
if not os.path.exists(sessions_dir):
|
|
606
|
-
if self.debug:
|
|
607
|
-
print("❌ No sessions directory found")
|
|
608
|
-
return
|
|
697
|
+
print("🔄 System tray load session requested...")
|
|
609
698
|
|
|
610
|
-
#
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
show_toast_notification("No saved sessions found", debug=self.debug)
|
|
619
|
-
except:
|
|
620
|
-
print("📂 No saved sessions found")
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
# For now, load the most recent session
|
|
624
|
-
# TODO: Add proper file picker dialog
|
|
625
|
-
session_files.sort(reverse=True) # Most recent first
|
|
626
|
-
latest_session = session_files[0]
|
|
627
|
-
filepath = os.path.join(sessions_dir, latest_session)
|
|
628
|
-
|
|
629
|
-
# Load session
|
|
630
|
-
success = self.llm_manager.load_session(filepath)
|
|
699
|
+
# CRITICAL: System tray actions MUST NOT automatically replace sessions
|
|
700
|
+
# Use the bubble's load_session method which includes proper file picker
|
|
701
|
+
if hasattr(self, 'bubble_manager') and self.bubble_manager:
|
|
702
|
+
# Delegate to bubble manager which has proper user file selection
|
|
703
|
+
bubble = self.bubble_manager.get_current_bubble()
|
|
704
|
+
if bubble:
|
|
705
|
+
bubble.load_session() # This includes user file picker dialog
|
|
706
|
+
return
|
|
631
707
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
708
|
+
# Fallback: Show notification that loading requires UI interaction
|
|
709
|
+
try:
|
|
710
|
+
from .ui.toast_window import show_toast_notification
|
|
711
|
+
show_toast_notification(
|
|
712
|
+
"To load session, please use the Load button in the chat interface",
|
|
713
|
+
debug=self.debug
|
|
714
|
+
)
|
|
715
|
+
except:
|
|
716
|
+
print("💬 To load session, please use the Load button in the chat interface")
|
|
717
|
+
|
|
718
|
+
if self.debug:
|
|
719
|
+
print("⚠️ System tray load session requires user file selection via UI")
|
|
644
720
|
|
|
645
721
|
except Exception as e:
|
|
646
722
|
if self.debug:
|
|
647
|
-
print(f"❌ Error
|
|
723
|
+
print(f"❌ Error in load session request: {e}")
|
|
648
724
|
|
|
649
725
|
def _preflight_initialization(self):
|
|
650
726
|
"""Pre-initialize components for instant bubble display on first click."""
|
|
@@ -667,7 +743,7 @@ class AbstractAssistantApp:
|
|
|
667
743
|
# Set up callbacks
|
|
668
744
|
self.bubble_manager.set_response_callback(self.handle_bubble_response)
|
|
669
745
|
self.bubble_manager.set_error_callback(self.handle_bubble_error)
|
|
670
|
-
|
|
746
|
+
# Note: Status callback will be set after preflight initialization to avoid TTS init interference
|
|
671
747
|
self.bubble_manager.set_app_quit_callback(self.quit_application)
|
|
672
748
|
|
|
673
749
|
if self.debug:
|
|
@@ -680,6 +756,12 @@ class AbstractAssistantApp:
|
|
|
680
756
|
# This creates the bubble without showing it
|
|
681
757
|
self.bubble_manager._prepare_bubble()
|
|
682
758
|
|
|
759
|
+
# Now set the status callback after TTS initialization is complete
|
|
760
|
+
if self.bubble_manager:
|
|
761
|
+
self.bubble_manager.set_status_callback(self.update_icon_status)
|
|
762
|
+
if self.debug:
|
|
763
|
+
print("✅ Status callback set after TTS initialization")
|
|
764
|
+
|
|
683
765
|
if self.debug:
|
|
684
766
|
print("✅ Preflight initialization completed - bubble ready for instant display")
|
|
685
767
|
|
|
@@ -687,10 +769,21 @@ class AbstractAssistantApp:
|
|
|
687
769
|
if self.debug:
|
|
688
770
|
print(f"⚠️ Preflight initialization failed: {e}")
|
|
689
771
|
print(" First click will still work but with delay")
|
|
772
|
+
|
|
773
|
+
# Still set status callback even if preflight failed
|
|
774
|
+
if self.bubble_manager:
|
|
775
|
+
self.bubble_manager.set_status_callback(self.update_icon_status)
|
|
690
776
|
|
|
691
777
|
def quit_application(self, icon=None, item=None):
|
|
692
778
|
"""Quit the application gracefully."""
|
|
779
|
+
if self.debug:
|
|
780
|
+
print("🔄 Quitting AbstractAssistant...")
|
|
781
|
+
|
|
693
782
|
self.is_running = False
|
|
783
|
+
|
|
784
|
+
# Stop animation timer
|
|
785
|
+
self._stop_animation_timer()
|
|
786
|
+
|
|
694
787
|
if self.icon:
|
|
695
788
|
self.icon.stop()
|
|
696
789
|
|
|
@@ -701,6 +794,9 @@ class AbstractAssistantApp:
|
|
|
701
794
|
except Exception as e:
|
|
702
795
|
if self.debug:
|
|
703
796
|
print(f"Error destroying bubble manager: {e}")
|
|
797
|
+
|
|
798
|
+
if self.debug:
|
|
799
|
+
print("✅ AbstractAssistant quit successfully")
|
|
704
800
|
|
|
705
801
|
def run(self):
|
|
706
802
|
"""Start the application using Qt event loop for proper threading."""
|
|
@@ -721,7 +817,7 @@ class AbstractAssistantApp:
|
|
|
721
817
|
|
|
722
818
|
# Check if system tray is available
|
|
723
819
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
|
724
|
-
print("❌ System tray is not available on this system")
|
|
820
|
+
print("❌ System tray is not available on this system") # Always show this error
|
|
725
821
|
return
|
|
726
822
|
|
|
727
823
|
# Create Qt-based system tray icon
|
|
@@ -730,31 +826,42 @@ class AbstractAssistantApp:
|
|
|
730
826
|
# Preflight initialization: Pre-load bubble manager for instant display
|
|
731
827
|
self._preflight_initialization()
|
|
732
828
|
|
|
733
|
-
|
|
734
|
-
|
|
829
|
+
if not self.debug:
|
|
830
|
+
print("AbstractAssistant started. Check your menu bar!")
|
|
831
|
+
print("Click the icon to open the chat interface.")
|
|
832
|
+
else:
|
|
833
|
+
print("AbstractAssistant started. Check your menu bar!")
|
|
834
|
+
print("Click the icon to open the chat interface.")
|
|
735
835
|
|
|
736
836
|
# Run Qt event loop (this blocks until quit)
|
|
737
837
|
self.qt_app.exec_()
|
|
738
838
|
|
|
739
839
|
except ImportError:
|
|
740
|
-
|
|
840
|
+
if self.debug:
|
|
841
|
+
print("❌ PyQt5 not available. Falling back to pystray...")
|
|
741
842
|
# Fallback to original pystray implementation
|
|
742
843
|
self.icon = self.create_system_tray_icon()
|
|
743
844
|
self.icon.run()
|
|
744
845
|
|
|
745
846
|
def _create_qt_system_tray_icon(self):
|
|
746
|
-
"""Create Qt-based system tray icon with
|
|
847
|
+
"""Create Qt-based system tray icon with smooth animations."""
|
|
747
848
|
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
|
|
748
849
|
from PyQt5.QtCore import QTimer
|
|
749
850
|
from PyQt5.QtGui import QIcon, QPixmap
|
|
750
851
|
from PIL import Image
|
|
751
852
|
import io
|
|
752
853
|
|
|
753
|
-
#
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
854
|
+
# Load base icon (same as pystray version)
|
|
855
|
+
self.base_icon = self._load_app_bundle_icon()
|
|
856
|
+
if not self.base_icon:
|
|
857
|
+
# Generate a base icon if app bundle icon not available
|
|
858
|
+
self.base_icon = self.icon_generator.create_app_icon(
|
|
859
|
+
color_scheme="green", # Ready state: steady green
|
|
860
|
+
animated=False # Ready state: no animation
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Apply initial smooth heartbeat effect
|
|
864
|
+
icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, "ready")
|
|
758
865
|
|
|
759
866
|
# Convert PIL image to QPixmap
|
|
760
867
|
img_buffer = io.BytesIO()
|
|
@@ -794,11 +901,20 @@ class AbstractAssistantApp:
|
|
|
794
901
|
|
|
795
902
|
tray_icon.setContextMenu(context_menu)
|
|
796
903
|
|
|
904
|
+
# Store reference for animations
|
|
905
|
+
self.qt_tray_icon = tray_icon
|
|
906
|
+
|
|
797
907
|
# Show the tray icon
|
|
798
908
|
tray_icon.show()
|
|
799
909
|
|
|
910
|
+
# Start initial animation
|
|
911
|
+
if self.debug:
|
|
912
|
+
print(f"🎨 Starting initial animation with status: ready")
|
|
913
|
+
print(f" Current status in app: {self.current_status}")
|
|
914
|
+
self._start_heartbeat_animation("ready")
|
|
915
|
+
|
|
800
916
|
if self.debug:
|
|
801
|
-
print("✅ Qt-based system tray icon created
|
|
917
|
+
print("✅ Qt-based system tray icon created with smooth animations")
|
|
802
918
|
|
|
803
919
|
return tray_icon
|
|
804
920
|
|