abstractassistant 0.2.0__tar.gz → 0.2.6__tar.gz

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.
Files changed (61) hide show
  1. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/PKG-INFO +28 -5
  2. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/README.md +24 -1
  3. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/core/llm_manager.py +15 -11
  4. abstractassistant-0.2.6/abstractassistant/create_app_bundle.py +48 -0
  5. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/history_dialog.py +142 -42
  6. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/qt_bubble.py +231 -58
  7. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/toast_window.py +8 -8
  8. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/ui_styles.py +2 -2
  9. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/utils/markdown_renderer.py +1 -1
  10. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant.egg-info/PKG-INFO +28 -5
  11. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant.egg-info/SOURCES.txt +2 -0
  12. abstractassistant-0.2.6/abstractassistant.egg-info/entry_points.txt +3 -0
  13. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant.egg-info/requires.txt +2 -1
  14. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant.egg-info/top_level.txt +1 -0
  15. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/pyproject.toml +8 -4
  16. abstractassistant-0.2.6/setup_macos_app.py +269 -0
  17. abstractassistant-0.2.0/abstractassistant.egg-info/entry_points.txt +0 -2
  18. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/LICENSE +0 -0
  19. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/__init__.py +0 -0
  20. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/app.py +0 -0
  21. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/cli.py +0 -0
  22. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/config.py +0 -0
  23. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/core/__init__.py +0 -0
  24. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/core/tts_manager.py +0 -0
  25. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/__init__.py +0 -0
  26. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/chat_bubble.py +0 -0
  27. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/provider_manager.py +0 -0
  28. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/toast_manager.py +0 -0
  29. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/ui/tts_state_manager.py +0 -0
  30. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/utils/__init__.py +0 -0
  31. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/utils/icon_generator.py +0 -0
  32. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant/web_server.py +0 -0
  33. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/abstractassistant.egg-info/dependency_links.txt +0 -0
  34. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/setup.cfg +0 -0
  35. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_abstractcore.py +0 -0
  36. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_app.py +0 -0
  37. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_bubble.py +0 -0
  38. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_bubble_manual.py +0 -0
  39. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_complete_voice_integration.py +0 -0
  40. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_complete_working.py +0 -0
  41. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_corrected_logic.py +0 -0
  42. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_double_click_fix.py +0 -0
  43. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_final_app_demo.py +0 -0
  44. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_final_verification.py +0 -0
  45. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_final_voice_mode.py +0 -0
  46. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_fixed_app.py +0 -0
  47. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_fixed_integration.py +0 -0
  48. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_full_voice_mode.py +0 -0
  49. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_integration.py +0 -0
  50. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_qt_bubble.py +0 -0
  51. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_qt_threading.py +0 -0
  52. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_real_application.py +0 -0
  53. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_reason_values.py +0 -0
  54. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_safe_startup.py +0 -0
  55. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_safe_voice_controls.py +0 -0
  56. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_simple.py +0 -0
  57. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_system_tray.py +0 -0
  58. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_timestamp_clicks.py +0 -0
  59. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_voice_double_click.py +0 -0
  60. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_voice_features.py +0 -0
  61. {abstractassistant-0.2.0 → abstractassistant-0.2.6}/tests/test_voice_timing.py +0 -0
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractassistant
3
- Version: 0.2.0
3
+ Version: 0.2.6
4
4
  Summary: A sleek (macOS) system tray application providing instant access to LLMs
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/lpalbou/abstractassistant
8
8
  Project-URL: Repository, https://github.com/lpalbou/abstractassistant
9
9
  Project-URL: Issues, https://github.com/lpalbou/abstractassistant/issues
10
10
  Keywords: ai,llm,macos,system-tray,assistant
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: End Users/Desktop
13
- Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Operating System :: MacOS
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.9
@@ -22,12 +21,13 @@ Classifier: Topic :: Desktop Environment
22
21
  Requires-Python: >=3.9
23
22
  Description-Content-Type: text/markdown
24
23
  License-File: LICENSE
25
- Requires-Dist: abstractcore[all]>=2.4.2
24
+ Requires-Dist: abstractcore[all]>=2.4.5
26
25
  Requires-Dist: pystray>=0.19.4
27
26
  Requires-Dist: Pillow>=10.0.0
28
27
  Requires-Dist: PyQt5>=5.15.0
29
28
  Requires-Dist: markdown>=3.5.0
30
29
  Requires-Dist: pygments>=2.16.0
30
+ Requires-Dist: pymdown-extensions>=10.0
31
31
  Requires-Dist: abstractvoice>=0.5.0
32
32
  Requires-Dist: pyperclip>=1.8.2
33
33
  Requires-Dist: plyer>=2.1.0
@@ -66,17 +66,40 @@ A sleek macOS system tray application providing instant access to Large Language
66
66
  ## 🚀 Quick Start
67
67
 
68
68
  ### 1. Installation
69
+
70
+ #### 🍎 macOS Users (Recommended)
71
+ ```bash
72
+ # Enhanced installation with Dock integration
73
+ python3 install.py
74
+ ```
75
+
76
+ This will:
77
+ - Install AbstractAssistant from PyPI
78
+ - Create a macOS app bundle in `/Applications`
79
+ - Add AbstractAssistant to your Dock for easy access
80
+
81
+ #### 🔧 Standard Installation
69
82
  ```bash
70
- # Install from PyPI (recommended)
83
+ # Install from PyPI
71
84
  pip install abstractassistant
72
85
  ```
73
86
 
74
87
  For detailed installation instructions including prerequisites and voice setup, see **[📖 Installation Guide](docs/installation.md)**.
75
88
 
76
89
  ### 2. First Launch
90
+
91
+ #### 🍎 macOS App Bundle Users
92
+ - **Dock**: Click the AbstractAssistant icon in your Dock
93
+ - **Spotlight**: Search for "AbstractAssistant" and press Enter
94
+ - **Finder**: Open `/Applications/AbstractAssistant.app`
95
+
96
+ #### 🔧 Terminal Users
77
97
  ```bash
78
98
  # Launch the assistant
79
99
  assistant
100
+
101
+ # Or create macOS app bundle after installation
102
+ create-app-bundle
80
103
  ```
81
104
 
82
105
  ### 3. Start Using
@@ -24,17 +24,40 @@ A sleek macOS system tray application providing instant access to Large Language
24
24
  ## 🚀 Quick Start
25
25
 
26
26
  ### 1. Installation
27
+
28
+ #### 🍎 macOS Users (Recommended)
29
+ ```bash
30
+ # Enhanced installation with Dock integration
31
+ python3 install.py
32
+ ```
33
+
34
+ This will:
35
+ - Install AbstractAssistant from PyPI
36
+ - Create a macOS app bundle in `/Applications`
37
+ - Add AbstractAssistant to your Dock for easy access
38
+
39
+ #### 🔧 Standard Installation
27
40
  ```bash
28
- # Install from PyPI (recommended)
41
+ # Install from PyPI
29
42
  pip install abstractassistant
30
43
  ```
31
44
 
32
45
  For detailed installation instructions including prerequisites and voice setup, see **[📖 Installation Guide](docs/installation.md)**.
33
46
 
34
47
  ### 2. First Launch
48
+
49
+ #### 🍎 macOS App Bundle Users
50
+ - **Dock**: Click the AbstractAssistant icon in your Dock
51
+ - **Spotlight**: Search for "AbstractAssistant" and press Enter
52
+ - **Finder**: Open `/Applications/AbstractAssistant.app`
53
+
54
+ #### 🔧 Terminal Users
35
55
  ```bash
36
56
  # Launch the assistant
37
57
  assistant
58
+
59
+ # Or create macOS app bundle after installation
60
+ create-app-bundle
38
61
  ```
39
62
 
40
63
  ### 3. Start Using
@@ -242,14 +242,15 @@ class LLMManager:
242
242
  self.current_model = model
243
243
  self._initialize_llm()
244
244
 
245
- def generate_response(self, message: str, provider: str = None, model: str = None) -> str:
245
+ def generate_response(self, message: str, provider: str = None, model: str = None, media: Optional[List[str]] = None) -> str:
246
246
  """Generate a response using the session for context persistence.
247
-
247
+
248
248
  Args:
249
249
  message: User message
250
250
  provider: Optional provider override
251
251
  model: Optional model override
252
-
252
+ media: Optional list of file paths to attach (images, PDFs, Office docs, etc.)
253
+
253
254
  Returns:
254
255
  Generated response text
255
256
  """
@@ -258,24 +259,27 @@ class LLMManager:
258
259
  self.set_provider(provider, model)
259
260
  elif model and model != self.current_model:
260
261
  self.set_model(model)
261
-
262
+
262
263
  try:
263
264
  # Ensure we have a session
264
265
  if self.current_session is None:
265
266
  self.create_new_session()
266
-
267
- # Generate response using session - CLEAN AND SIMPLE
268
- # response = session.generate('What is my name?') # Remembers context
269
- response = self.current_session.generate(message)
270
-
267
+
268
+ # Generate response using session with optional media files
269
+ # AbstractCore 2.4.5+ supports media=[] parameter for file attachments
270
+ if media and len(media) > 0:
271
+ response = self.current_session.generate(message, media=media)
272
+ else:
273
+ response = self.current_session.generate(message)
274
+
271
275
  # Handle response format
272
276
  if hasattr(response, 'content'):
273
277
  response_text = response.content
274
278
  else:
275
279
  response_text = str(response)
276
-
280
+
277
281
  return response_text
278
-
282
+
279
283
  except Exception as e:
280
284
  return f"Error generating response: {str(e)}"
281
285
 
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Create macOS App Bundle for AbstractAssistant.
4
+
5
+ This script can be run after installation to create a macOS app bundle
6
+ that allows launching AbstractAssistant from the Dock.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ def main():
14
+ """Create macOS app bundle for AbstractAssistant."""
15
+ try:
16
+ # Import the app bundle generator
17
+ from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
18
+
19
+ # Find the package directory
20
+ import abstractassistant
21
+ package_dir = Path(abstractassistant.__file__).parent
22
+
23
+ # Create the generator and build the app bundle
24
+ generator = MacOSAppBundleGenerator(package_dir)
25
+
26
+ print("🍎 Creating macOS app bundle for AbstractAssistant...")
27
+ success = generator.generate_app_bundle()
28
+
29
+ if success:
30
+ print("\n🎉 Success!")
31
+ print(" AbstractAssistant is now available in your Applications folder")
32
+ print(" You can launch it from the Dock or Spotlight!")
33
+ return 0
34
+ else:
35
+ print("\n❌ Failed to create app bundle")
36
+ return 1
37
+
38
+ except ImportError as e:
39
+ print(f"❌ Error: {e}")
40
+ print(" Make sure AbstractAssistant is properly installed")
41
+ return 1
42
+ except Exception as e:
43
+ print(f"❌ Unexpected error: {e}")
44
+ return 1
45
+
46
+
47
+ if __name__ == "__main__":
48
+ sys.exit(main())
@@ -6,23 +6,99 @@ This module provides an authentic iPhone Messages UI for displaying chat history
6
6
  import re
7
7
  from datetime import datetime
8
8
  from typing import Dict, List
9
+ import markdown
10
+ from markdown.extensions.fenced_code import FencedCodeExtension
11
+ from markdown.extensions.tables import TableExtension
12
+ from markdown.extensions.nl2br import Nl2BrExtension
13
+ from pygments import highlight
14
+ from pygments.lexers import get_lexer_by_name, TextLexer
15
+ from pygments.formatters import HtmlFormatter
9
16
 
10
17
  try:
11
18
  from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
12
- QWidget, QLabel, QFrame, QPushButton)
13
- from PyQt6.QtCore import Qt
14
- from PyQt6.QtGui import QFont
19
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
20
+ from PyQt6.QtCore import Qt, QTimer, pyqtSignal
21
+ from PyQt6.QtGui import QFont, QCursor
15
22
  except ImportError:
16
23
  try:
17
24
  from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
18
- QWidget, QLabel, QFrame, QPushButton)
19
- from PyQt5.QtCore import Qt
20
- from PyQt5.QtGui import QFont
25
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
26
+ from PyQt5.QtCore import Qt, QTimer, pyqtSignal
27
+ from PyQt5.QtGui import QFont, QCursor
21
28
  except ImportError:
22
29
  from PySide2.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
23
- QWidget, QLabel, QFrame, QPushButton)
24
- from PySide2.QtCore import Qt
25
- from PySide2.QtGui import QFont
30
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
31
+ from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal
32
+ from PySide2.QtGui import QFont, QCursor
33
+
34
+
35
+ class ClickableBubble(QFrame):
36
+ """Clickable message bubble that copies content to clipboard."""
37
+
38
+ clicked = pyqtSignal()
39
+
40
+ def __init__(self, content: str, is_user: bool, parent=None):
41
+ super().__init__(parent)
42
+ self.content = content
43
+ self.is_user = is_user
44
+ self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
45
+
46
+ # Store original colors for animation
47
+ if is_user:
48
+ self.normal_bg = "#007AFF"
49
+ self.clicked_bg = "#0066CC"
50
+ else:
51
+ self.normal_bg = "#3a3a3c"
52
+ self.clicked_bg = "#4a4a4c"
53
+
54
+ def mousePressEvent(self, event):
55
+ """Handle mouse press with visual feedback."""
56
+ 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
+ """)
66
+ super().mousePressEvent(event)
67
+
68
+ def mouseReleaseEvent(self, event):
69
+ """Handle mouse release - copy to clipboard and restore style."""
70
+ 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()
90
+ super().mouseReleaseEvent(event)
91
+
92
+ def _restore_normal_style(self):
93
+ """Restore normal bubble style."""
94
+ self.setStyleSheet(f"""
95
+ QFrame {{
96
+ background: {self.normal_bg};
97
+ border: none;
98
+ border-radius: 18px;
99
+ max-width: 400px;
100
+ }}
101
+ """)
26
102
 
27
103
 
28
104
  class SafeDialog(QDialog):
@@ -97,6 +173,9 @@ class iPhoneMessagesDialog:
97
173
  # Apply authentic iPhone styling
98
174
  dialog.setStyleSheet(iPhoneMessagesDialog._get_authentic_iphone_styles())
99
175
 
176
+ # Auto-scroll to bottom to show the latest messages
177
+ QTimer.singleShot(100, lambda: scroll_area.verticalScrollBar().setValue(scroll_area.verticalScrollBar().maximum()))
178
+
100
179
  return dialog
101
180
 
102
181
  @staticmethod
@@ -161,7 +240,7 @@ class iPhoneMessagesDialog:
161
240
  background: transparent;
162
241
  border: none;
163
242
  text-align: left;
164
- font-family: -apple-system;
243
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
165
244
  }
166
245
  """)
167
246
  nav_layout.addWidget(back_btn)
@@ -175,7 +254,7 @@ class iPhoneMessagesDialog:
175
254
  color: #ffffff;
176
255
  font-size: 17px;
177
256
  font-weight: 600;
178
- font-family: -apple-system;
257
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
179
258
  }
180
259
  """)
181
260
  nav_layout.addWidget(title)
@@ -216,20 +295,20 @@ class iPhoneMessagesDialog:
216
295
  container = QFrame()
217
296
  container.setStyleSheet("background: transparent; border: none;")
218
297
  layout = QHBoxLayout(container)
219
- layout.setContentsMargins(16, 0, 16, 0) # iPhone margins
298
+ layout.setContentsMargins(12, 0, 12, 0) # Tighter margins for more width
220
299
  layout.setSpacing(0)
221
300
 
222
- # Create bubble
223
- bubble = QFrame()
301
+ # Create clickable bubble
302
+ bubble = ClickableBubble(msg['content'], is_user)
224
303
  bubble_layout = QVBoxLayout(bubble)
225
- bubble_layout.setContentsMargins(13, 8, 13, 8) # iPhone padding
304
+ bubble_layout.setContentsMargins(12, 7, 12, 7) # More compact padding
226
305
  bubble_layout.setSpacing(0)
227
306
 
228
307
  # Process content with FULL markdown support
229
308
  content = iPhoneMessagesDialog._process_full_markdown(msg['content'])
230
309
  content_label = QLabel(content)
231
310
  content_label.setWordWrap(True)
232
- content_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
311
+ content_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) # No text selection, bubble handles clicks
233
312
  content_label.setTextFormat(Qt.TextFormat.RichText)
234
313
 
235
314
  if is_user:
@@ -239,17 +318,17 @@ class iPhoneMessagesDialog:
239
318
  background: #007AFF;
240
319
  border: none;
241
320
  border-radius: 18px;
242
- max-width: 320px;
321
+ max-width: 400px;
243
322
  }
244
323
  """)
245
324
  content_label.setStyleSheet("""
246
325
  QLabel {
247
326
  background: transparent;
248
327
  color: #FFFFFF;
249
- font-size: 17px;
328
+ font-size: 14px;
250
329
  font-weight: 400;
251
- line-height: 22px;
252
- font-family: -apple-system;
330
+ line-height: 18px;
331
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
253
332
  }
254
333
  """)
255
334
  # Right align
@@ -262,17 +341,17 @@ class iPhoneMessagesDialog:
262
341
  background: #3a3a3c;
263
342
  border: none;
264
343
  border-radius: 18px;
265
- max-width: 320px;
344
+ max-width: 400px;
266
345
  }
267
346
  """)
268
347
  content_label.setStyleSheet("""
269
348
  QLabel {
270
349
  background: transparent;
271
350
  color: #ffffff;
272
- font-size: 17px;
351
+ font-size: 14px;
273
352
  font-weight: 400;
274
- line-height: 22px;
275
- font-family: -apple-system;
353
+ line-height: 18px;
354
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
276
355
  }
277
356
  """)
278
357
  # Left align
@@ -315,7 +394,7 @@ class iPhoneMessagesDialog:
315
394
  font-size: 13px;
316
395
  font-weight: 400;
317
396
  color: rgba(255, 255, 255, 0.6);
318
- font-family: -apple-system;
397
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
319
398
  padding: 0px;
320
399
  }
321
400
  """)
@@ -340,29 +419,50 @@ class iPhoneMessagesDialog:
340
419
 
341
420
  @staticmethod
342
421
  def _process_full_markdown(text: str) -> str:
343
- """Process markdown formatting for iPhone Messages display."""
344
- # Convert **bold** to <strong>bold</strong>
345
- text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text)
422
+ """Process markdown using proper markdown library with syntax highlighting."""
423
+ # Configure markdown with extensions
424
+ md = markdown.Markdown(
425
+ extensions=[
426
+ FencedCodeExtension(),
427
+ TableExtension(),
428
+ 'nl2br', # Convert newlines to <br>
429
+ ],
430
+ extension_configs={
431
+ 'fenced_code': {
432
+ 'lang_prefix': 'language-',
433
+ }
434
+ }
435
+ )
436
+
437
+ # Convert markdown to HTML
438
+ html = md.convert(text)
346
439
 
347
- # Convert *italic* to <em>italic</em>
348
- text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text)
440
+ # Apply custom styling to the generated HTML
441
+ # Style code blocks
442
+ html = html.replace('<pre>', '<pre style="margin: 6px 0; background: rgba(0,0,0,0.3); border-radius: 6px; padding: 8px; overflow-x: auto;">')
443
+ html = html.replace('<code>', '<code style="font-family: \'SF Mono\', \'Menlo\', \'Monaco\', \'Courier New\', monospace; font-size: 12px; line-height: 1.4; color: #e8e8e8;">')
349
444
 
350
- # Convert `code` to inline code
351
- text = re.sub(r'`([^`]+)`', r'<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px; font-family: monospace;">\1</code>', text)
445
+ # Style tables
446
+ html = html.replace('<table>', '<table style="margin: 6px 0; border-collapse: collapse; width: 100%; font-size: 12px;">')
447
+ html = html.replace('<thead>', '<thead style="background: rgba(0,0,0,0.2);">')
448
+ html = html.replace('<th>', '<th style="padding: 4px 8px; text-align: left; font-weight: 600; border-bottom: 1px solid rgba(255,255,255,0.2);">')
449
+ html = html.replace('<td>', '<td style="padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.1);">')
352
450
 
353
- # Convert headers
354
- text = re.sub(r'^#### (.*$)', r'<h4 style="margin: 8px 0 4px 0; font-weight: 600;">\1</h4>', text, flags=re.MULTILINE)
355
- text = re.sub(r'^### (.*$)', r'<h3 style="margin: 10px 0 5px 0; font-weight: 600;">\1</h3>', text, flags=re.MULTILINE)
356
- text = re.sub(r'^## (.*$)', r'<h2 style="margin: 12px 0 6px 0; font-weight: 600;">\1</h2>', text, flags=re.MULTILINE)
357
- text = re.sub(r'^# (.*$)', r'<h1 style="margin: 14px 0 7px 0; font-weight: 600;">\1</h1>', text, flags=re.MULTILINE)
451
+ # Style headers with minimal spacing
452
+ html = html.replace('<h1>', '<h1 style="margin: 6px 0 2px 0; font-weight: 600; font-size: 17px;">')
453
+ html = html.replace('<h2>', '<h2 style="margin: 5px 0 2px 0; font-weight: 600; font-size: 16px;">')
454
+ html = html.replace('<h3>', '<h3 style="margin: 4px 0 1px 0; font-weight: 600; font-size: 15px;">')
455
+ html = html.replace('<h4>', '<h4 style="margin: 3px 0 1px 0; font-weight: 600; font-size: 14px;">')
358
456
 
359
- # Convert bullet points
360
- text = re.sub(r'^[•\-\*] (.*)$', r'<p style="margin: 2px 0; padding-left: 16px;">• \1</p>', text, flags=re.MULTILINE)
457
+ # Style lists with minimal spacing
458
+ html = html.replace('<ul>', '<ul style="margin: 4px 0; padding-left: 20px;">')
459
+ html = html.replace('<ol>', '<ol style="margin: 4px 0; padding-left: 20px;">')
460
+ html = html.replace('<li>', '<li style="margin: 1px 0; line-height: 1.3;">')
361
461
 
362
- # Convert line breaks to HTML
363
- text = text.replace('\n', '<br>')
462
+ # Style paragraphs with minimal spacing
463
+ html = html.replace('<p>', '<p style="margin: 2px 0; line-height: 1.3;">')
364
464
 
365
- return text
465
+ return html
366
466
 
367
467
  @staticmethod
368
468
  def _get_authentic_iphone_styles() -> str: