abstractassistant 0.3.3__py3-none-any.whl → 0.3.5__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/ui/history_dialog.py +575 -51
- abstractassistant/ui/provider_manager.py +2 -2
- abstractassistant/ui/qt_bubble.py +454 -29
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/METADATA +3 -3
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/RECORD +9 -9
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/WHEEL +0 -0
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.3.3.dist-info → abstractassistant-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -4,8 +4,9 @@ iPhone Messages-style history dialog for AbstractAssistant.
|
|
|
4
4
|
This module provides an authentic iPhone Messages UI for displaying chat history.
|
|
5
5
|
"""
|
|
6
6
|
import re
|
|
7
|
+
import time
|
|
7
8
|
from datetime import datetime
|
|
8
|
-
from typing import Dict, List
|
|
9
|
+
from typing import Dict, List, Callable, Optional
|
|
9
10
|
import markdown
|
|
10
11
|
from markdown.extensions.fenced_code import FencedCodeExtension
|
|
11
12
|
from markdown.extensions.tables import TableExtension
|
|
@@ -33,62 +34,194 @@ except ImportError:
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class ClickableBubble(QFrame):
|
|
36
|
-
"""Clickable message bubble that copies content to clipboard."""
|
|
37
|
+
"""Clickable message bubble that copies content to clipboard and supports deletion."""
|
|
37
38
|
|
|
38
39
|
clicked = pyqtSignal()
|
|
40
|
+
delete_requested = pyqtSignal(int) # Signal with message index
|
|
41
|
+
selection_changed = pyqtSignal(int, bool) # Signal with message index and selection state
|
|
39
42
|
|
|
40
|
-
def __init__(self, content: str, is_user: bool, parent=None):
|
|
43
|
+
def __init__(self, content: str, is_user: bool, message_index: int, parent=None):
|
|
41
44
|
super().__init__(parent)
|
|
42
45
|
self.content = content
|
|
43
46
|
self.is_user = is_user
|
|
47
|
+
self.message_index = message_index
|
|
48
|
+
self.is_selected = False
|
|
49
|
+
self.selection_mode = False
|
|
44
50
|
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
45
51
|
|
|
46
52
|
# Store original colors for animation
|
|
47
53
|
if is_user:
|
|
48
54
|
self.normal_bg = "#007AFF"
|
|
49
55
|
self.clicked_bg = "#0066CC"
|
|
56
|
+
self.selected_bg = "#FF3B30" # Red for selection
|
|
50
57
|
else:
|
|
51
58
|
self.normal_bg = "#3a3a3c"
|
|
52
59
|
self.clicked_bg = "#4a4a4c"
|
|
60
|
+
self.selected_bg = "#FF3B30" # Red for selection
|
|
61
|
+
|
|
62
|
+
# Long press timer for selection mode
|
|
63
|
+
self.long_press_timer = QTimer()
|
|
64
|
+
self.long_press_timer.setSingleShot(True)
|
|
65
|
+
self.long_press_timer.timeout.connect(self._start_selection_mode)
|
|
66
|
+
self.press_start_time = None
|
|
53
67
|
|
|
54
68
|
def mousePressEvent(self, event):
|
|
55
|
-
"""Handle mouse press with visual feedback."""
|
|
69
|
+
"""Handle mouse press with visual feedback and long press detection."""
|
|
56
70
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
self.press_start_time = time.time()
|
|
72
|
+
|
|
73
|
+
if self.selection_mode:
|
|
74
|
+
# In selection mode, toggle selection
|
|
75
|
+
self.toggle_selection()
|
|
76
|
+
else:
|
|
77
|
+
# Normal mode - start long press timer and apply clicked style
|
|
78
|
+
self.long_press_timer.start(800) # 800ms for long press
|
|
79
|
+
self.setStyleSheet(f"""
|
|
80
|
+
QFrame {{
|
|
81
|
+
background: {self.clicked_bg};
|
|
82
|
+
border: none;
|
|
83
|
+
border-radius: 18px;
|
|
84
|
+
max-width: 400px;
|
|
85
|
+
}}
|
|
86
|
+
""")
|
|
66
87
|
super().mousePressEvent(event)
|
|
67
88
|
|
|
68
89
|
def mouseReleaseEvent(self, event):
|
|
69
|
-
"""Handle mouse release - copy to clipboard and restore style."""
|
|
90
|
+
"""Handle mouse release - copy to clipboard, handle selection, and restore style."""
|
|
70
91
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
# Stop long press timer
|
|
93
|
+
self.long_press_timer.stop()
|
|
94
|
+
|
|
95
|
+
if self.selection_mode:
|
|
96
|
+
# In selection mode, just restore style
|
|
97
|
+
self._update_visual_state()
|
|
98
|
+
else:
|
|
99
|
+
# Check if this was a quick tap (not long press)
|
|
100
|
+
if self.press_start_time and (time.time() - self.press_start_time) < 0.5:
|
|
101
|
+
# Copy to clipboard
|
|
102
|
+
clipboard = QApplication.clipboard()
|
|
103
|
+
clipboard.setText(self.content)
|
|
104
|
+
|
|
105
|
+
# Visual feedback: glossy effect (lighter color briefly)
|
|
106
|
+
glossy_color = "#0080FF" if self.is_user else "#5a5a5c"
|
|
107
|
+
self.setStyleSheet(f"""
|
|
108
|
+
QFrame {{
|
|
109
|
+
background: {glossy_color};
|
|
110
|
+
border: none;
|
|
111
|
+
border-radius: 18px;
|
|
112
|
+
max-width: 400px;
|
|
113
|
+
}}
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
# Restore normal color after brief delay
|
|
117
|
+
QTimer.singleShot(200, self._restore_normal_style)
|
|
118
|
+
|
|
119
|
+
self.clicked.emit()
|
|
120
|
+
else:
|
|
121
|
+
# Long press - restore style immediately
|
|
122
|
+
self._restore_normal_style()
|
|
90
123
|
super().mouseReleaseEvent(event)
|
|
91
124
|
|
|
125
|
+
def _start_selection_mode(self):
|
|
126
|
+
"""Start selection mode on long press."""
|
|
127
|
+
self.selection_mode = True
|
|
128
|
+
self.is_selected = True
|
|
129
|
+
self._update_visual_state()
|
|
130
|
+
self.selection_changed.emit(self.message_index, True)
|
|
131
|
+
|
|
132
|
+
# Provide haptic-like feedback by briefly changing color
|
|
133
|
+
self.setStyleSheet(f"""
|
|
134
|
+
QFrame {{
|
|
135
|
+
background: {self.selected_bg};
|
|
136
|
+
border: 2px solid #FFFFFF;
|
|
137
|
+
border-radius: 18px;
|
|
138
|
+
max-width: 400px;
|
|
139
|
+
}}
|
|
140
|
+
""")
|
|
141
|
+
|
|
142
|
+
# Add selection indicator
|
|
143
|
+
QTimer.singleShot(100, self._update_visual_state)
|
|
144
|
+
|
|
145
|
+
def toggle_selection(self):
|
|
146
|
+
"""Toggle selection state in selection mode."""
|
|
147
|
+
self.is_selected = not self.is_selected
|
|
148
|
+
self._update_selection_circle()
|
|
149
|
+
self._update_visual_state()
|
|
150
|
+
self.selection_changed.emit(self.message_index, self.is_selected)
|
|
151
|
+
|
|
152
|
+
def set_selection_mode(self, enabled: bool):
|
|
153
|
+
"""Set selection mode state."""
|
|
154
|
+
self.selection_mode = enabled
|
|
155
|
+
if not enabled:
|
|
156
|
+
self.is_selected = False
|
|
157
|
+
|
|
158
|
+
# Show/hide selection circle
|
|
159
|
+
if hasattr(self, 'selection_circle'):
|
|
160
|
+
if enabled:
|
|
161
|
+
self.selection_circle.show()
|
|
162
|
+
else:
|
|
163
|
+
self.selection_circle.hide()
|
|
164
|
+
|
|
165
|
+
self._update_selection_circle()
|
|
166
|
+
self._update_visual_state()
|
|
167
|
+
|
|
168
|
+
def set_selected(self, selected: bool):
|
|
169
|
+
"""Set selection state."""
|
|
170
|
+
self.is_selected = selected
|
|
171
|
+
self._update_selection_circle()
|
|
172
|
+
self._update_visual_state()
|
|
173
|
+
|
|
174
|
+
def _update_selection_circle(self):
|
|
175
|
+
"""Update selection circle appearance."""
|
|
176
|
+
if hasattr(self, 'selection_circle') and self.selection_mode:
|
|
177
|
+
if self.is_selected:
|
|
178
|
+
# Selected state - filled circle with checkmark
|
|
179
|
+
self.selection_circle.setStyleSheet("""
|
|
180
|
+
QPushButton {
|
|
181
|
+
background: #007AFF;
|
|
182
|
+
border: 2px solid #007AFF;
|
|
183
|
+
border-radius: 11px;
|
|
184
|
+
margin: 0px;
|
|
185
|
+
padding: 0px;
|
|
186
|
+
color: white;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
font-weight: bold;
|
|
189
|
+
}
|
|
190
|
+
QPushButton:hover {
|
|
191
|
+
background: #0066CC;
|
|
192
|
+
border: 2px solid #0066CC;
|
|
193
|
+
}
|
|
194
|
+
""")
|
|
195
|
+
self.selection_circle.setText("✓")
|
|
196
|
+
else:
|
|
197
|
+
# Unselected state - empty circle
|
|
198
|
+
self.selection_circle.setStyleSheet("""
|
|
199
|
+
QPushButton {
|
|
200
|
+
background: transparent;
|
|
201
|
+
border: 2px solid rgba(255, 255, 255, 0.6);
|
|
202
|
+
border-radius: 11px;
|
|
203
|
+
margin: 0px;
|
|
204
|
+
padding: 0px;
|
|
205
|
+
}
|
|
206
|
+
QPushButton:hover {
|
|
207
|
+
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
208
|
+
}
|
|
209
|
+
""")
|
|
210
|
+
self.selection_circle.setText("")
|
|
211
|
+
|
|
212
|
+
def _update_visual_state(self):
|
|
213
|
+
"""Update visual state based on selection - no visual changes to bubble itself."""
|
|
214
|
+
# In authentic iPhone Messages, the bubble appearance doesn't change
|
|
215
|
+
# Only the selection circle changes state - bubble stays the same
|
|
216
|
+
self.setStyleSheet(f"""
|
|
217
|
+
QFrame {{
|
|
218
|
+
background: {self.normal_bg};
|
|
219
|
+
border: none;
|
|
220
|
+
border-radius: 18px;
|
|
221
|
+
max-width: 400px;
|
|
222
|
+
}}
|
|
223
|
+
""")
|
|
224
|
+
|
|
92
225
|
def _restore_normal_style(self):
|
|
93
226
|
"""Restore normal bubble style."""
|
|
94
227
|
self.setStyleSheet(f"""
|
|
@@ -107,13 +240,322 @@ class SafeDialog(QDialog):
|
|
|
107
240
|
def __init__(self, parent=None):
|
|
108
241
|
super().__init__(parent)
|
|
109
242
|
self.hide_callback = None
|
|
243
|
+
self.delete_callback = None
|
|
244
|
+
self.selection_mode = False
|
|
245
|
+
self.selected_messages = set()
|
|
246
|
+
self.message_bubbles = []
|
|
247
|
+
self.trash_button = None
|
|
248
|
+
self.edit_button = None
|
|
110
249
|
|
|
111
250
|
def set_hide_callback(self, callback):
|
|
112
251
|
"""Set callback to call when dialog is hidden."""
|
|
113
252
|
self.hide_callback = callback
|
|
114
253
|
|
|
254
|
+
def set_delete_callback(self, callback):
|
|
255
|
+
"""Set callback to call when messages are deleted."""
|
|
256
|
+
self.delete_callback = callback
|
|
257
|
+
|
|
258
|
+
def enter_selection_mode(self):
|
|
259
|
+
"""Enter selection mode for message deletion."""
|
|
260
|
+
self.selection_mode = True
|
|
261
|
+
self.selected_messages.clear()
|
|
262
|
+
|
|
263
|
+
# Update all bubbles to selection mode
|
|
264
|
+
for bubble in self.message_bubbles:
|
|
265
|
+
bubble.set_selection_mode(True)
|
|
266
|
+
|
|
267
|
+
# Update navigation bar
|
|
268
|
+
self._update_navbar_for_selection_mode()
|
|
269
|
+
|
|
270
|
+
def exit_selection_mode(self):
|
|
271
|
+
"""Exit selection mode."""
|
|
272
|
+
self.selection_mode = False
|
|
273
|
+
self.selected_messages.clear()
|
|
274
|
+
|
|
275
|
+
# Update all bubbles to normal mode
|
|
276
|
+
for bubble in self.message_bubbles:
|
|
277
|
+
bubble.set_selection_mode(False)
|
|
278
|
+
|
|
279
|
+
# Update navigation bar
|
|
280
|
+
self._update_navbar_for_normal_mode()
|
|
281
|
+
|
|
282
|
+
def _update_navbar_for_selection_mode(self):
|
|
283
|
+
"""Update navbar for selection mode."""
|
|
284
|
+
if hasattr(self, 'edit_button'):
|
|
285
|
+
self.edit_button.setText("Cancel")
|
|
286
|
+
self.edit_button.clicked.disconnect()
|
|
287
|
+
self.edit_button.clicked.connect(self.exit_selection_mode)
|
|
288
|
+
|
|
289
|
+
# Hide trash button initially (will show when messages are selected)
|
|
290
|
+
if hasattr(self, 'trash_button'):
|
|
291
|
+
self.trash_button.hide()
|
|
292
|
+
|
|
293
|
+
def _update_navbar_for_normal_mode(self):
|
|
294
|
+
"""Update navbar for normal mode."""
|
|
295
|
+
if hasattr(self, 'edit_button'):
|
|
296
|
+
self.edit_button.setText("Edit")
|
|
297
|
+
self.edit_button.clicked.disconnect()
|
|
298
|
+
self.edit_button.clicked.connect(self.enter_selection_mode)
|
|
299
|
+
|
|
300
|
+
# Hide trash button
|
|
301
|
+
if hasattr(self, 'trash_button'):
|
|
302
|
+
self.trash_button.hide()
|
|
303
|
+
|
|
304
|
+
def on_selection_changed(self, message_index: int, selected: bool):
|
|
305
|
+
"""Handle selection change from a bubble."""
|
|
306
|
+
if selected:
|
|
307
|
+
self.selected_messages.add(message_index)
|
|
308
|
+
else:
|
|
309
|
+
self.selected_messages.discard(message_index)
|
|
310
|
+
|
|
311
|
+
# Show/hide trash button based on selection
|
|
312
|
+
if hasattr(self, 'trash_button'):
|
|
313
|
+
if len(self.selected_messages) > 0:
|
|
314
|
+
self.trash_button.show()
|
|
315
|
+
else:
|
|
316
|
+
self.trash_button.hide()
|
|
317
|
+
|
|
318
|
+
def delete_selected_messages(self):
|
|
319
|
+
"""Delete selected messages with iPhone-style bottom action sheet."""
|
|
320
|
+
try:
|
|
321
|
+
if not self.selected_messages or not self.delete_callback:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Show iPhone-style bottom action sheet
|
|
325
|
+
count = len(self.selected_messages)
|
|
326
|
+
self._show_delete_action_sheet(count)
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
import traceback
|
|
330
|
+
traceback.print_exc()
|
|
331
|
+
|
|
332
|
+
def _show_delete_action_sheet(self, count: int):
|
|
333
|
+
"""Show iPhone-style bottom action sheet for deletion confirmation."""
|
|
334
|
+
# Create overlay widget that covers the entire dialog
|
|
335
|
+
overlay = QWidget(self)
|
|
336
|
+
overlay.setStyleSheet("background: rgba(0, 0, 0, 0.4);")
|
|
337
|
+
overlay.resize(self.size())
|
|
338
|
+
overlay.move(0, 0)
|
|
339
|
+
|
|
340
|
+
# Create action sheet container
|
|
341
|
+
action_sheet = QWidget(overlay)
|
|
342
|
+
action_sheet.setStyleSheet("""
|
|
343
|
+
QWidget {
|
|
344
|
+
background: #2C2C2E;
|
|
345
|
+
border-radius: 13px;
|
|
346
|
+
border: none;
|
|
347
|
+
}
|
|
348
|
+
""")
|
|
349
|
+
|
|
350
|
+
# Layout for action sheet
|
|
351
|
+
layout = QVBoxLayout(action_sheet)
|
|
352
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
353
|
+
layout.setSpacing(1)
|
|
354
|
+
|
|
355
|
+
# Delete button (red, destructive)
|
|
356
|
+
delete_btn = QPushButton(f"Delete {count} Message{'s' if count > 1 else ''}")
|
|
357
|
+
delete_btn.setStyleSheet("""
|
|
358
|
+
QPushButton {
|
|
359
|
+
background: #FF3B30;
|
|
360
|
+
color: white;
|
|
361
|
+
border: none;
|
|
362
|
+
padding: 16px;
|
|
363
|
+
font-size: 17px;
|
|
364
|
+
font-weight: 400;
|
|
365
|
+
text-align: center;
|
|
366
|
+
}
|
|
367
|
+
QPushButton:hover {
|
|
368
|
+
background: #D70015;
|
|
369
|
+
}
|
|
370
|
+
""")
|
|
371
|
+
delete_btn.clicked.connect(lambda: self._confirm_deletion(overlay))
|
|
372
|
+
|
|
373
|
+
# Cancel button
|
|
374
|
+
cancel_btn = QPushButton("Cancel")
|
|
375
|
+
cancel_btn.setStyleSheet("""
|
|
376
|
+
QPushButton {
|
|
377
|
+
background: #48484A;
|
|
378
|
+
color: #007AFF;
|
|
379
|
+
border: none;
|
|
380
|
+
padding: 16px;
|
|
381
|
+
font-size: 17px;
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
text-align: center;
|
|
384
|
+
border-radius: 13px;
|
|
385
|
+
margin-top: 8px;
|
|
386
|
+
}
|
|
387
|
+
QPushButton:hover {
|
|
388
|
+
background: #5A5A5C;
|
|
389
|
+
}
|
|
390
|
+
""")
|
|
391
|
+
cancel_btn.clicked.connect(lambda: self._cancel_deletion(overlay))
|
|
392
|
+
|
|
393
|
+
layout.addWidget(delete_btn)
|
|
394
|
+
layout.addWidget(cancel_btn)
|
|
395
|
+
|
|
396
|
+
# Position action sheet at bottom
|
|
397
|
+
action_sheet.setFixedWidth(self.width() - 40)
|
|
398
|
+
action_sheet.adjustSize()
|
|
399
|
+
action_sheet.move(20, self.height() - action_sheet.height() - 20)
|
|
400
|
+
|
|
401
|
+
# Show overlay and action sheet
|
|
402
|
+
overlay.show()
|
|
403
|
+
overlay.raise_()
|
|
404
|
+
|
|
405
|
+
# Store reference for cleanup
|
|
406
|
+
self.current_overlay = overlay
|
|
407
|
+
|
|
408
|
+
def _confirm_deletion(self, overlay):
|
|
409
|
+
"""Confirm deletion and execute it."""
|
|
410
|
+
try:
|
|
411
|
+
# Hide overlay
|
|
412
|
+
overlay.hide()
|
|
413
|
+
overlay.deleteLater()
|
|
414
|
+
self.current_overlay = None
|
|
415
|
+
|
|
416
|
+
# Convert to sorted list for consistent deletion
|
|
417
|
+
indices_to_delete = sorted(list(self.selected_messages), reverse=True)
|
|
418
|
+
|
|
419
|
+
# Call the delete callback with error handling
|
|
420
|
+
if self.delete_callback:
|
|
421
|
+
self.delete_callback(indices_to_delete)
|
|
422
|
+
|
|
423
|
+
# Exit selection mode after deletion
|
|
424
|
+
self.exit_selection_mode()
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
import traceback
|
|
428
|
+
traceback.print_exc()
|
|
429
|
+
|
|
430
|
+
# Try to exit selection mode even if deletion failed
|
|
431
|
+
try:
|
|
432
|
+
self.exit_selection_mode()
|
|
433
|
+
except:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
def _cancel_deletion(self, overlay):
|
|
437
|
+
"""Cancel deletion and hide action sheet."""
|
|
438
|
+
overlay.hide()
|
|
439
|
+
overlay.deleteLater()
|
|
440
|
+
self.current_overlay = None
|
|
441
|
+
|
|
442
|
+
def update_message_history(self, new_message_history: List[Dict]):
|
|
443
|
+
"""Update the dialog with new message history without closing it."""
|
|
444
|
+
try:
|
|
445
|
+
# Exit selection mode if active
|
|
446
|
+
if self.selection_mode:
|
|
447
|
+
self.exit_selection_mode()
|
|
448
|
+
|
|
449
|
+
# Clear existing content completely using correct widget references
|
|
450
|
+
if hasattr(self, 'messages_layout'):
|
|
451
|
+
layout = self.messages_layout
|
|
452
|
+
|
|
453
|
+
# Remove all widgets except the stretch at the end
|
|
454
|
+
while layout.count() > 0:
|
|
455
|
+
child = layout.takeAt(0)
|
|
456
|
+
if child.widget():
|
|
457
|
+
widget = child.widget()
|
|
458
|
+
widget.setParent(None)
|
|
459
|
+
widget.deleteLater()
|
|
460
|
+
|
|
461
|
+
# Force immediate processing of deleteLater calls
|
|
462
|
+
from PyQt5.QtCore import QCoreApplication
|
|
463
|
+
QCoreApplication.processEvents()
|
|
464
|
+
|
|
465
|
+
# Recreate messages with new history
|
|
466
|
+
if new_message_history:
|
|
467
|
+
# Re-add messages using the same method as original creation
|
|
468
|
+
iPhoneMessagesDialog._add_authentic_iphone_messages(layout, new_message_history, self)
|
|
469
|
+
else:
|
|
470
|
+
# Add placeholder for empty state
|
|
471
|
+
placeholder = QLabel("No messages")
|
|
472
|
+
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
473
|
+
placeholder.setStyleSheet("""
|
|
474
|
+
QLabel {
|
|
475
|
+
color: rgba(255, 255, 255, 0.5);
|
|
476
|
+
font-size: 16px;
|
|
477
|
+
padding: 40px;
|
|
478
|
+
}
|
|
479
|
+
""")
|
|
480
|
+
layout.addWidget(placeholder)
|
|
481
|
+
|
|
482
|
+
# Re-add stretch to push messages to top
|
|
483
|
+
layout.addStretch()
|
|
484
|
+
|
|
485
|
+
# Force complete UI update
|
|
486
|
+
if hasattr(self, 'messages_widget'):
|
|
487
|
+
self.messages_widget.adjustSize()
|
|
488
|
+
self.messages_widget.update()
|
|
489
|
+
self.messages_widget.repaint()
|
|
490
|
+
|
|
491
|
+
if hasattr(self, 'scroll_area'):
|
|
492
|
+
self.scroll_area.update()
|
|
493
|
+
self.scroll_area.repaint()
|
|
494
|
+
# Scroll to bottom to show latest messages
|
|
495
|
+
QTimer.singleShot(100, lambda: self.scroll_area.verticalScrollBar().setValue(
|
|
496
|
+
self.scroll_area.verticalScrollBar().maximum()
|
|
497
|
+
))
|
|
498
|
+
|
|
499
|
+
# Update the entire dialog
|
|
500
|
+
self.update()
|
|
501
|
+
self.repaint()
|
|
502
|
+
|
|
503
|
+
else:
|
|
504
|
+
raise Exception("Dialog structure not found")
|
|
505
|
+
|
|
506
|
+
except Exception as e:
|
|
507
|
+
import traceback
|
|
508
|
+
traceback.print_exc()
|
|
509
|
+
raise # Re-raise to trigger fallback in qt_bubble.py
|
|
510
|
+
|
|
511
|
+
def _populate_messages(self, message_history: List[Dict]):
|
|
512
|
+
"""Populate the scroll area with message bubbles."""
|
|
513
|
+
if not hasattr(self, 'scroll_content'):
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
layout = self.scroll_content.layout()
|
|
517
|
+
if not layout:
|
|
518
|
+
layout = QVBoxLayout(self.scroll_content)
|
|
519
|
+
layout.setContentsMargins(20, 20, 20, 20)
|
|
520
|
+
layout.setSpacing(8)
|
|
521
|
+
|
|
522
|
+
# Add messages as bubbles (only if there are messages)
|
|
523
|
+
if message_history:
|
|
524
|
+
for i, message in enumerate(message_history):
|
|
525
|
+
if message.get('type') in ['user', 'assistant']:
|
|
526
|
+
bubble = ClickableBubble(
|
|
527
|
+
message.get('content', ''),
|
|
528
|
+
message.get('type') == 'user',
|
|
529
|
+
i,
|
|
530
|
+
self
|
|
531
|
+
)
|
|
532
|
+
layout.addWidget(bubble)
|
|
533
|
+
else:
|
|
534
|
+
# If no messages, add a placeholder
|
|
535
|
+
placeholder = QLabel("No messages")
|
|
536
|
+
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
537
|
+
placeholder.setStyleSheet("""
|
|
538
|
+
QLabel {
|
|
539
|
+
color: rgba(255, 255, 255, 0.5);
|
|
540
|
+
font-size: 16px;
|
|
541
|
+
padding: 40px;
|
|
542
|
+
}
|
|
543
|
+
""")
|
|
544
|
+
layout.addWidget(placeholder)
|
|
545
|
+
|
|
546
|
+
# Add stretch to push messages to top
|
|
547
|
+
layout.addStretch()
|
|
548
|
+
|
|
549
|
+
# Force layout update
|
|
550
|
+
self.scroll_content.update()
|
|
551
|
+
if hasattr(self, 'scroll_area'):
|
|
552
|
+
self.scroll_area.update()
|
|
553
|
+
|
|
554
|
+
|
|
115
555
|
def closeEvent(self, event):
|
|
116
556
|
"""Override close event to hide instead of close."""
|
|
557
|
+
if self.selection_mode:
|
|
558
|
+
self.exit_selection_mode()
|
|
117
559
|
event.ignore()
|
|
118
560
|
self.hide()
|
|
119
561
|
if self.hide_callback:
|
|
@@ -121,6 +563,8 @@ class SafeDialog(QDialog):
|
|
|
121
563
|
|
|
122
564
|
def reject(self):
|
|
123
565
|
"""Override reject to hide instead of close."""
|
|
566
|
+
if self.selection_mode:
|
|
567
|
+
self.exit_selection_mode()
|
|
124
568
|
self.hide()
|
|
125
569
|
if self.hide_callback:
|
|
126
570
|
self.hide_callback()
|
|
@@ -130,14 +574,22 @@ class iPhoneMessagesDialog:
|
|
|
130
574
|
"""Create authentic iPhone Messages-style chat history dialog."""
|
|
131
575
|
|
|
132
576
|
@staticmethod
|
|
133
|
-
def create_dialog(message_history: List[Dict], parent=None) -> QDialog:
|
|
134
|
-
"""Create AUTHENTIC iPhone Messages dialog
|
|
577
|
+
def create_dialog(message_history: List[Dict], parent=None, delete_callback: Optional[Callable] = None) -> QDialog:
|
|
578
|
+
"""Create AUTHENTIC iPhone Messages dialog with deletion support."""
|
|
579
|
+
# Safety check for empty message history
|
|
580
|
+
if not message_history:
|
|
581
|
+
return None
|
|
582
|
+
|
|
135
583
|
dialog = SafeDialog(parent)
|
|
136
584
|
dialog.setWindowTitle("Messages")
|
|
137
585
|
dialog.setModal(False)
|
|
138
586
|
dialog.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
139
587
|
dialog.resize(504, 650) # Increased width by 20% (420 * 1.2 = 504)
|
|
140
588
|
|
|
589
|
+
# Set delete callback
|
|
590
|
+
if delete_callback:
|
|
591
|
+
dialog.set_delete_callback(delete_callback)
|
|
592
|
+
|
|
141
593
|
# Position dialog near right edge of screen like iPhone
|
|
142
594
|
iPhoneMessagesDialog._position_dialog_right(dialog)
|
|
143
595
|
|
|
@@ -146,10 +598,10 @@ class iPhoneMessagesDialog:
|
|
|
146
598
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
147
599
|
main_layout.setSpacing(0)
|
|
148
600
|
|
|
149
|
-
# iPhone navigation bar
|
|
601
|
+
# iPhone navigation bar with delete button
|
|
150
602
|
navbar = iPhoneMessagesDialog._create_authentic_navbar(dialog)
|
|
151
603
|
main_layout.addWidget(navbar)
|
|
152
|
-
|
|
604
|
+
|
|
153
605
|
# Messages container with pure white background
|
|
154
606
|
scroll_area = QScrollArea()
|
|
155
607
|
scroll_area.setWidgetResizable(True)
|
|
@@ -163,8 +615,13 @@ class iPhoneMessagesDialog:
|
|
|
163
615
|
messages_layout.setContentsMargins(0, 16, 0, 16) # iPhone spacing
|
|
164
616
|
messages_layout.setSpacing(0)
|
|
165
617
|
|
|
166
|
-
#
|
|
167
|
-
|
|
618
|
+
# Store references for updating
|
|
619
|
+
dialog.scroll_area = scroll_area
|
|
620
|
+
dialog.messages_widget = messages_widget
|
|
621
|
+
dialog.messages_layout = messages_layout
|
|
622
|
+
|
|
623
|
+
# Add messages with authentic iPhone styling and deletion support
|
|
624
|
+
iPhoneMessagesDialog._add_authentic_iphone_messages(messages_layout, message_history, dialog)
|
|
168
625
|
|
|
169
626
|
messages_layout.addStretch()
|
|
170
627
|
scroll_area.setWidget(messages_widget)
|
|
@@ -203,8 +660,8 @@ class iPhoneMessagesDialog:
|
|
|
203
660
|
dialog.move(x, y)
|
|
204
661
|
|
|
205
662
|
@staticmethod
|
|
206
|
-
def _create_authentic_navbar(dialog:
|
|
207
|
-
"""Create AUTHENTIC iPhone Messages navigation bar."""
|
|
663
|
+
def _create_authentic_navbar(dialog: SafeDialog) -> QFrame:
|
|
664
|
+
"""Create AUTHENTIC iPhone Messages navigation bar with delete functionality."""
|
|
208
665
|
navbar = QFrame()
|
|
209
666
|
navbar.setFixedHeight(94) # iPhone status bar + nav bar
|
|
210
667
|
navbar.setStyleSheet("""
|
|
@@ -261,18 +718,57 @@ class iPhoneMessagesDialog:
|
|
|
261
718
|
|
|
262
719
|
nav_layout.addStretch()
|
|
263
720
|
|
|
721
|
+
# Trash icon (initially hidden, appears when messages are selected)
|
|
722
|
+
trash_btn = QPushButton("🗑️")
|
|
723
|
+
trash_btn.setFixedSize(30, 30)
|
|
724
|
+
trash_btn.clicked.connect(dialog.delete_selected_messages)
|
|
725
|
+
trash_btn.setStyleSheet("""
|
|
726
|
+
QPushButton {
|
|
727
|
+
color: #FF3B30;
|
|
728
|
+
font-size: 18px;
|
|
729
|
+
background: transparent;
|
|
730
|
+
border: none;
|
|
731
|
+
text-align: center;
|
|
732
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
733
|
+
}
|
|
734
|
+
QPushButton:hover {
|
|
735
|
+
background: rgba(255, 59, 48, 0.1);
|
|
736
|
+
border-radius: 15px;
|
|
737
|
+
}
|
|
738
|
+
""")
|
|
739
|
+
trash_btn.hide() # Initially hidden
|
|
740
|
+
dialog.trash_button = trash_btn # Store reference
|
|
741
|
+
nav_layout.addWidget(trash_btn)
|
|
742
|
+
|
|
743
|
+
# Delete button (Edit in iPhone style)
|
|
744
|
+
edit_btn = QPushButton("Edit")
|
|
745
|
+
edit_btn.clicked.connect(dialog.enter_selection_mode)
|
|
746
|
+
edit_btn.setStyleSheet("""
|
|
747
|
+
QPushButton {
|
|
748
|
+
color: #007AFF;
|
|
749
|
+
font-size: 17px;
|
|
750
|
+
font-weight: 400;
|
|
751
|
+
background: transparent;
|
|
752
|
+
border: none;
|
|
753
|
+
text-align: right;
|
|
754
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
755
|
+
}
|
|
756
|
+
""")
|
|
757
|
+
dialog.edit_button = edit_btn # Store reference
|
|
758
|
+
nav_layout.addWidget(edit_btn)
|
|
759
|
+
|
|
264
760
|
layout.addWidget(nav_frame)
|
|
265
761
|
return navbar
|
|
266
762
|
|
|
267
763
|
@staticmethod
|
|
268
|
-
def _add_authentic_iphone_messages(layout: QVBoxLayout, message_history: List[Dict]):
|
|
269
|
-
"""Add messages with AUTHENTIC iPhone Messages styling."""
|
|
764
|
+
def _add_authentic_iphone_messages(layout: QVBoxLayout, message_history: List[Dict], dialog: SafeDialog):
|
|
765
|
+
"""Add messages with AUTHENTIC iPhone Messages styling and deletion support."""
|
|
270
766
|
for index, msg in enumerate(message_history):
|
|
271
767
|
message_type = msg.get('type', msg.get('role', 'unknown'))
|
|
272
768
|
is_user = message_type in ['user', 'human']
|
|
273
769
|
|
|
274
|
-
# Create authentic iPhone bubble
|
|
275
|
-
bubble_container = iPhoneMessagesDialog._create_authentic_iphone_bubble(msg, is_user, index, message_history)
|
|
770
|
+
# Create authentic iPhone bubble with deletion support
|
|
771
|
+
bubble_container = iPhoneMessagesDialog._create_authentic_iphone_bubble(msg, is_user, index, message_history, dialog)
|
|
276
772
|
layout.addWidget(bubble_container)
|
|
277
773
|
|
|
278
774
|
# Add spacing between messages (6px like iPhone)
|
|
@@ -283,8 +779,8 @@ class iPhoneMessagesDialog:
|
|
|
283
779
|
layout.addWidget(spacer)
|
|
284
780
|
|
|
285
781
|
@staticmethod
|
|
286
|
-
def _create_authentic_iphone_bubble(msg: Dict, is_user: bool, index: int, message_history: List[Dict]) -> QFrame:
|
|
287
|
-
"""Create AUTHENTIC iPhone Messages bubble
|
|
782
|
+
def _create_authentic_iphone_bubble(msg: Dict, is_user: bool, index: int, message_history: List[Dict], dialog: SafeDialog) -> QFrame:
|
|
783
|
+
"""Create AUTHENTIC iPhone Messages bubble with deletion support."""
|
|
288
784
|
main_container = QFrame()
|
|
289
785
|
main_container.setStyleSheet("background: transparent; border: none;")
|
|
290
786
|
main_layout = QVBoxLayout(main_container)
|
|
@@ -298,8 +794,32 @@ class iPhoneMessagesDialog:
|
|
|
298
794
|
layout.setContentsMargins(12, 0, 12, 0) # Tighter margins for more width
|
|
299
795
|
layout.setSpacing(0)
|
|
300
796
|
|
|
301
|
-
# Create
|
|
302
|
-
|
|
797
|
+
# Create selection circle (initially hidden)
|
|
798
|
+
selection_circle = QPushButton()
|
|
799
|
+
selection_circle.setFixedSize(22, 22)
|
|
800
|
+
selection_circle.setStyleSheet("""
|
|
801
|
+
QPushButton {
|
|
802
|
+
background: transparent;
|
|
803
|
+
border: 2px solid rgba(255, 255, 255, 0.6);
|
|
804
|
+
border-radius: 11px;
|
|
805
|
+
margin: 0px;
|
|
806
|
+
padding: 0px;
|
|
807
|
+
}
|
|
808
|
+
QPushButton:hover {
|
|
809
|
+
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
810
|
+
}
|
|
811
|
+
""")
|
|
812
|
+
selection_circle.hide() # Initially hidden
|
|
813
|
+
|
|
814
|
+
# Create clickable bubble with deletion support
|
|
815
|
+
bubble = ClickableBubble(msg['content'], is_user, index)
|
|
816
|
+
bubble.selection_changed.connect(dialog.on_selection_changed)
|
|
817
|
+
bubble.selection_circle = selection_circle # Store reference
|
|
818
|
+
dialog.message_bubbles.append(bubble) # Track bubbles for selection mode
|
|
819
|
+
|
|
820
|
+
# Connect selection circle click
|
|
821
|
+
selection_circle.clicked.connect(bubble.toggle_selection)
|
|
822
|
+
|
|
303
823
|
bubble_layout = QVBoxLayout(bubble)
|
|
304
824
|
bubble_layout.setContentsMargins(12, 7, 12, 7) # More compact padding
|
|
305
825
|
bubble_layout.setSpacing(0)
|
|
@@ -331,8 +851,10 @@ class iPhoneMessagesDialog:
|
|
|
331
851
|
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
332
852
|
}
|
|
333
853
|
""")
|
|
334
|
-
# Right align
|
|
854
|
+
# Right align - selection circle on the left of bubble (towards center)
|
|
335
855
|
layout.addStretch()
|
|
856
|
+
layout.addWidget(selection_circle, 0, Qt.AlignmentFlag.AlignCenter)
|
|
857
|
+
layout.addSpacing(8) # Small gap between circle and bubble
|
|
336
858
|
layout.addWidget(bubble)
|
|
337
859
|
else:
|
|
338
860
|
# Received bubble: Light gray with black text
|
|
@@ -354,8 +876,10 @@ class iPhoneMessagesDialog:
|
|
|
354
876
|
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
355
877
|
}
|
|
356
878
|
""")
|
|
357
|
-
# Left align
|
|
879
|
+
# Left align - selection circle on the right of bubble (towards center)
|
|
358
880
|
layout.addWidget(bubble)
|
|
881
|
+
layout.addSpacing(8) # Small gap between bubble and circle
|
|
882
|
+
layout.addWidget(selection_circle, 0, Qt.AlignmentFlag.AlignCenter)
|
|
359
883
|
layout.addStretch()
|
|
360
884
|
|
|
361
885
|
bubble_layout.addWidget(content_label)
|