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.
@@ -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
- # Apply clicked style (darker)
58
- self.setStyleSheet(f"""
59
- QFrame {{
60
- background: {self.clicked_bg};
61
- border: none;
62
- border-radius: 18px;
63
- max-width: 400px;
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
- # Copy to clipboard
72
- clipboard = QApplication.clipboard()
73
- clipboard.setText(self.content)
74
-
75
- # Visual feedback: glossy effect (lighter color briefly)
76
- glossy_color = "#0080FF" if self.is_user else "#5a5a5c"
77
- self.setStyleSheet(f"""
78
- QFrame {{
79
- background: {glossy_color};
80
- border: none;
81
- border-radius: 18px;
82
- max-width: 400px;
83
- }}
84
- """)
85
-
86
- # Restore normal color after brief delay
87
- QTimer.singleShot(200, self._restore_normal_style)
88
-
89
- self.clicked.emit()
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 - EXACTLY like the real app."""
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
- # Add messages with authentic iPhone styling
167
- iPhoneMessagesDialog._add_authentic_iphone_messages(messages_layout, message_history)
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: QDialog) -> QFrame:
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 - exactly like real iPhone."""
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 clickable bubble
302
- bubble = ClickableBubble(msg['content'], is_user)
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)