abstractassistant 0.2.0__tar.gz → 0.2.5__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 (58) hide show
  1. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/PKG-INFO +3 -2
  2. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/core/llm_manager.py +15 -11
  3. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/history_dialog.py +142 -42
  4. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/qt_bubble.py +231 -58
  5. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/toast_window.py +8 -8
  6. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/ui_styles.py +2 -2
  7. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/utils/markdown_renderer.py +1 -1
  8. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/PKG-INFO +3 -2
  9. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/requires.txt +2 -1
  10. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/pyproject.toml +3 -2
  11. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/LICENSE +0 -0
  12. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/README.md +0 -0
  13. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/__init__.py +0 -0
  14. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/app.py +0 -0
  15. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/cli.py +0 -0
  16. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/config.py +0 -0
  17. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/core/__init__.py +0 -0
  18. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/core/tts_manager.py +0 -0
  19. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/__init__.py +0 -0
  20. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/chat_bubble.py +0 -0
  21. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/provider_manager.py +0 -0
  22. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/toast_manager.py +0 -0
  23. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/ui/tts_state_manager.py +0 -0
  24. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/utils/__init__.py +0 -0
  25. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/utils/icon_generator.py +0 -0
  26. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant/web_server.py +0 -0
  27. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/SOURCES.txt +0 -0
  28. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/dependency_links.txt +0 -0
  29. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/entry_points.txt +0 -0
  30. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/abstractassistant.egg-info/top_level.txt +0 -0
  31. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/setup.cfg +0 -0
  32. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_abstractcore.py +0 -0
  33. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_app.py +0 -0
  34. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_bubble.py +0 -0
  35. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_bubble_manual.py +0 -0
  36. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_complete_voice_integration.py +0 -0
  37. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_complete_working.py +0 -0
  38. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_corrected_logic.py +0 -0
  39. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_double_click_fix.py +0 -0
  40. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_final_app_demo.py +0 -0
  41. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_final_verification.py +0 -0
  42. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_final_voice_mode.py +0 -0
  43. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_fixed_app.py +0 -0
  44. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_fixed_integration.py +0 -0
  45. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_full_voice_mode.py +0 -0
  46. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_integration.py +0 -0
  47. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_qt_bubble.py +0 -0
  48. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_qt_threading.py +0 -0
  49. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_real_application.py +0 -0
  50. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_reason_values.py +0 -0
  51. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_safe_startup.py +0 -0
  52. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_safe_voice_controls.py +0 -0
  53. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_simple.py +0 -0
  54. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_system_tray.py +0 -0
  55. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_timestamp_clicks.py +0 -0
  56. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_voice_double_click.py +0 -0
  57. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_voice_features.py +0 -0
  58. {abstractassistant-0.2.0 → abstractassistant-0.2.5}/tests/test_voice_timing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractassistant
3
- Version: 0.2.0
3
+ Version: 0.2.5
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
6
  License: MIT
@@ -22,12 +22,13 @@ Classifier: Topic :: Desktop Environment
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: abstractcore[all]>=2.4.2
25
+ Requires-Dist: abstractcore[all]>=2.4.5
26
26
  Requires-Dist: pystray>=0.19.4
27
27
  Requires-Dist: Pillow>=10.0.0
28
28
  Requires-Dist: PyQt5>=5.15.0
29
29
  Requires-Dist: markdown>=3.5.0
30
30
  Requires-Dist: pygments>=2.16.0
31
+ Requires-Dist: pymdown-extensions>=10.0
31
32
  Requires-Dist: abstractvoice>=0.5.0
32
33
  Requires-Dist: pyperclip>=1.8.2
33
34
  Requires-Dist: plyer>=2.1.0
@@ -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
 
@@ -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:
@@ -145,7 +145,7 @@ class TTSToggle(QPushButton):
145
145
  border-radius: 12px;
146
146
  font-size: 12px;
147
147
  color: {text_color};
148
- font-family: -apple-system, system-ui, sans-serif;
148
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
149
149
  font-weight: 600;
150
150
  }}
151
151
  QPushButton:hover {{
@@ -212,7 +212,7 @@ class FullVoiceToggle(QPushButton):
212
212
  border-radius: 12px;
213
213
  font-size: 12px;
214
214
  color: {text_color};
215
- font-family: -apple-system, system-ui, sans-serif;
215
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
216
216
  font-weight: 600;
217
217
  }}
218
218
  QPushButton:hover {{
@@ -228,28 +228,34 @@ class FullVoiceToggle(QPushButton):
228
228
 
229
229
  class LLMWorker(QThread):
230
230
  """Worker thread for LLM processing."""
231
-
231
+
232
232
  response_ready = pyqtSignal(str)
233
233
  error_occurred = pyqtSignal(str)
234
-
235
- def __init__(self, llm_manager, message, provider, model):
234
+
235
+ def __init__(self, llm_manager, message, provider, model, media=None):
236
236
  super().__init__()
237
237
  self.llm_manager = llm_manager
238
238
  self.message = message
239
239
  self.provider = provider
240
240
  self.model = model
241
-
241
+ self.media = media or []
242
+
242
243
  def run(self):
243
244
  """Run LLM processing in background."""
244
245
  try:
245
- # Use LLMManager session for context persistence
246
- response = self.llm_manager.generate_response(self.message, self.provider, self.model)
247
-
246
+ # Use LLMManager session for context persistence with optional media files
247
+ response = self.llm_manager.generate_response(
248
+ self.message,
249
+ self.provider,
250
+ self.model,
251
+ media=self.media if self.media else None
252
+ )
253
+
248
254
  # Response is already a string from LLMManager
249
255
  response_text = str(response)
250
-
256
+
251
257
  self.response_ready.emit(response_text)
252
-
258
+
253
259
  except Exception as e:
254
260
  print(f"❌ LLM Error: {e}")
255
261
  import traceback
@@ -278,6 +284,9 @@ class QtChatBubble(QWidget):
278
284
 
279
285
  # History dialog instance for toggle behavior
280
286
  self.history_dialog = None
287
+
288
+ # Attached files for media handling (AbstractCore 2.4.5+)
289
+ self.attached_files: List[str] = []
281
290
 
282
291
  # Initialize new manager classes
283
292
  self.provider_manager = None
@@ -357,7 +366,7 @@ class QtChatBubble(QWidget):
357
366
  font-size: 14px;
358
367
  font-weight: 600;
359
368
  color: rgba(255, 255, 255, 0.9);
360
- font-family: -apple-system, system-ui, sans-serif;
369
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
361
370
  }
362
371
  QPushButton:hover {
363
372
  background: rgba(255, 60, 60, 0.8);
@@ -389,7 +398,7 @@ class QtChatBubble(QWidget):
389
398
  border-radius: 11px;
390
399
  font-size: 10px;
391
400
  color: rgba(255, 255, 255, 0.7);
392
- font-family: -apple-system, system-ui, sans-serif;
401
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
393
402
  padding: 0 10px;
394
403
  }
395
404
  QPushButton:hover {
@@ -431,7 +440,7 @@ class QtChatBubble(QWidget):
431
440
  font-size: 10px;
432
441
  font-weight: 600;
433
442
  color: #ffffff;
434
- font-family: -apple-system, system-ui, sans-serif;
443
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
435
444
  }
436
445
  """)
437
446
  header_layout.addWidget(self.status_label)
@@ -442,7 +451,7 @@ class QtChatBubble(QWidget):
442
451
  self.input_container = QFrame()
443
452
  self.input_container.setStyleSheet("""
444
453
  QFrame {
445
- background: #1e1e1e;
454
+ background: #2a2a2a;
446
455
  border: 1px solid #404040;
447
456
  border-radius: 8px;
448
457
  padding: 4px;
@@ -455,14 +464,42 @@ class QtChatBubble(QWidget):
455
464
  # Input field with inline send button
456
465
  input_row = QHBoxLayout()
457
466
  input_row.setSpacing(4)
458
-
467
+
468
+ # File attachment button - modern paperclip icon
469
+ self.attach_button = QPushButton("📎")
470
+ self.attach_button.clicked.connect(self.attach_files)
471
+ self.attach_button.setFixedSize(36, 36)
472
+ self.attach_button.setToolTip("Attach files (images, PDFs, Office docs, etc.)")
473
+ self.attach_button.setStyleSheet("""
474
+ QPushButton {
475
+ background: rgba(255, 255, 255, 0.08);
476
+ border: 1px solid #404040;
477
+ border-radius: 18px;
478
+ font-size: 14px;
479
+ color: rgba(255, 255, 255, 0.7);
480
+ text-align: center;
481
+ padding: 0px;
482
+ }
483
+
484
+ QPushButton:hover {
485
+ background: rgba(255, 255, 255, 0.12);
486
+ border: 1px solid #0066cc;
487
+ color: rgba(255, 255, 255, 0.9);
488
+ }
489
+
490
+ QPushButton:pressed {
491
+ background: rgba(255, 255, 255, 0.06);
492
+ }
493
+ """)
494
+ input_row.addWidget(self.attach_button)
495
+
459
496
  # Text input - larger, primary focus
460
497
  self.input_text = QTextEdit()
461
498
  self.input_text.setPlaceholderText("Ask me anything... (Shift+Enter to send)")
462
499
  self.input_text.setMaximumHeight(100) # Increased to better use available space
463
500
  self.input_text.setMinimumHeight(70) # Increased to better use available space
464
501
  input_row.addWidget(self.input_text)
465
-
502
+
466
503
  # Send button - primary action with special styling
467
504
  self.send_button = QPushButton("→")
468
505
  self.send_button.clicked.connect(self.send_message)
@@ -478,16 +515,16 @@ class QtChatBubble(QWidget):
478
515
  text-align: center;
479
516
  padding: 0px;
480
517
  }
481
-
518
+
482
519
  QPushButton:hover {
483
520
  background: #0080ff;
484
521
  border: 1px solid #0099ff;
485
522
  }
486
-
523
+
487
524
  QPushButton:pressed {
488
525
  background: #0052a3;
489
526
  }
490
-
527
+
491
528
  QPushButton:disabled {
492
529
  background: #404040;
493
530
  color: #666666;
@@ -495,14 +532,30 @@ class QtChatBubble(QWidget):
495
532
  }
496
533
  """)
497
534
  input_row.addWidget(self.send_button)
498
-
535
+
499
536
  input_layout.addLayout(input_row)
537
+
538
+ # Attached files display area (initially hidden)
539
+ self.attached_files_container = QFrame()
540
+ self.attached_files_container.setStyleSheet("""
541
+ QFrame {
542
+ background: rgba(255, 255, 255, 0.04);
543
+ border: 1px solid #404040;
544
+ border-radius: 6px;
545
+ padding: 4px;
546
+ }
547
+ """)
548
+ self.attached_files_layout = QHBoxLayout(self.attached_files_container)
549
+ self.attached_files_layout.setContentsMargins(4, 4, 4, 4)
550
+ self.attached_files_layout.setSpacing(4)
551
+ self.attached_files_container.hide() # Initially hidden
552
+ input_layout.addWidget(self.attached_files_container)
500
553
  layout.addWidget(self.input_container)
501
554
 
502
555
  # Bottom controls - Cursor style (minimal, clean)
503
556
  controls_layout = QHBoxLayout()
504
- controls_layout.setContentsMargins(12, 3, 12, 4)
505
- controls_layout.setSpacing(8)
557
+ controls_layout.setContentsMargins(8, 2, 8, 2)
558
+ controls_layout.setSpacing(4)
506
559
 
507
560
  # Provider dropdown (rounded, clean)
508
561
  self.provider_combo = QComboBox()
@@ -514,10 +567,10 @@ class QtChatBubble(QWidget):
514
567
  background: rgba(255, 255, 255, 0.08);
515
568
  border: none;
516
569
  border-radius: 14px;
517
- padding: 0 12px;
570
+ padding: 0 8px;
518
571
  font-size: 11px;
519
572
  color: rgba(255, 255, 255, 0.9);
520
- font-family: -apple-system, system-ui, sans-serif;
573
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
521
574
  }
522
575
  QComboBox:hover {
523
576
  background: rgba(255, 255, 255, 0.12);
@@ -544,10 +597,10 @@ class QtChatBubble(QWidget):
544
597
  background: rgba(255, 255, 255, 0.08);
545
598
  border: none;
546
599
  border-radius: 14px;
547
- padding: 0 12px;
600
+ padding: 0 8px;
548
601
  font-size: 11px;
549
602
  color: rgba(255, 255, 255, 0.9);
550
- font-family: -apple-system, system-ui, sans-serif;
603
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
551
604
  }
552
605
  QComboBox:hover {
553
606
  background: rgba(255, 255, 255, 0.12);
@@ -568,7 +621,7 @@ class QtChatBubble(QWidget):
568
621
 
569
622
  # Token counter (minimal)
570
623
  self.token_label = QLabel("0 / 128k")
571
- self.token_label.setFixedHeight(36) # Increased by 30% (28 * 1.3 = 36.4 ≈ 36)
624
+ self.token_label.setFixedHeight(28) # Match provider and model dropdown height
572
625
  self.token_label.setMinimumWidth(104) # Increased by 30% (80 * 1.3 = 104)
573
626
  self.token_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
574
627
  self.token_label.setStyleSheet("""
@@ -578,7 +631,7 @@ class QtChatBubble(QWidget):
578
631
  border-radius: 14px;
579
632
  font-size: 12px;
580
633
  color: rgba(255, 255, 255, 0.6);
581
- font-family: -apple-system, system-ui, sans-serif;
634
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
582
635
  }
583
636
  """)
584
637
  controls_layout.addWidget(self.token_label)
@@ -612,21 +665,21 @@ class QtChatBubble(QWidget):
612
665
 
613
666
  /* Input Field - Modern Grey Design */
614
667
  QTextEdit {
615
- background: #1e1e1e;
668
+ background: #2a2a2a;
616
669
  border: 1px solid #404040;
617
670
  border-radius: 8px;
618
671
  padding: 12px 16px;
619
672
  font-size: 14px;
620
673
  font-weight: 400;
621
674
  color: #ffffff;
622
- font-family: system-ui, -apple-system, sans-serif;
675
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
623
676
  selection-background-color: #0066cc;
624
677
  line-height: 1.4;
625
678
  }
626
679
 
627
680
  QTextEdit:focus {
628
681
  border: 1px solid #0066cc;
629
- background: #252525;
682
+ background: #333333;
630
683
  }
631
684
 
632
685
  QTextEdit::placeholder {
@@ -642,7 +695,7 @@ class QtChatBubble(QWidget):
642
695
  font-size: 11px;
643
696
  font-weight: 500;
644
697
  color: #ffffff;
645
- font-family: system-ui, -apple-system, sans-serif;
698
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
646
699
  }
647
700
 
648
701
  QPushButton:hover {
@@ -670,7 +723,7 @@ class QtChatBubble(QWidget):
670
723
  font-size: 12px;
671
724
  font-weight: 400;
672
725
  color: #ffffff;
673
- font-family: system-ui, -apple-system, sans-serif;
726
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
674
727
  letter-spacing: 0.01em;
675
728
  }
676
729
 
@@ -700,7 +753,7 @@ class QtChatBubble(QWidget):
700
753
  selection-background-color: #4299e1;
701
754
  color: #e2e8f0;
702
755
  padding: 4px;
703
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
756
+ font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
704
757
  }
705
758
 
706
759
  QComboBox QAbstractItemView::item {
@@ -732,7 +785,7 @@ class QtChatBubble(QWidget):
732
785
  color: rgba(255, 255, 255, 0.8);
733
786
  font-size: 12px;
734
787
  font-weight: 500;
735
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
788
+ font-family: "SF Pro Text", "Helvetica Neue", 'Segoe UI', Roboto, sans-serif;
736
789
  letter-spacing: 0.3px;
737
790
  }
738
791
 
@@ -747,7 +800,7 @@ class QtChatBubble(QWidget):
747
800
  text-transform: uppercase;
748
801
  letter-spacing: 0.5px;
749
802
  color: #a6e3a1;
750
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
803
+ font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
751
804
  }
752
805
 
753
806
  QLabel#status_generating {
@@ -760,7 +813,7 @@ class QtChatBubble(QWidget):
760
813
  text-transform: uppercase;
761
814
  letter-spacing: 0.5px;
762
815
  color: #fab387;
763
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
816
+ font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
764
817
  }
765
818
 
766
819
  QLabel#status_error {
@@ -773,7 +826,7 @@ class QtChatBubble(QWidget):
773
826
  text-transform: uppercase;
774
827
  letter-spacing: 0.5px;
775
828
  color: #f38ba8;
776
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
829
+ font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
777
830
  }
778
831
 
779
832
  QLabel#token_label {
@@ -781,7 +834,7 @@ class QtChatBubble(QWidget):
781
834
  border: 1px solid #4a5568;
782
835
  border-radius: 8px;
783
836
  padding: 10px 12px;
784
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
837
+ font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
785
838
  font-size: 11px;
786
839
  font-weight: 500;
787
840
  color: #cbd5e0;
@@ -1023,22 +1076,136 @@ class QtChatBubble(QWidget):
1023
1076
  print(f"Model changed to: {self.current_model}")
1024
1077
 
1025
1078
 
1079
+ def attach_files(self):
1080
+ """Open file dialog to attach files (AbstractCore 2.4.5+ media handling)."""
1081
+ file_dialog = QFileDialog(self)
1082
+ file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
1083
+ file_dialog.setNameFilter(
1084
+ "All supported files (*.png *.jpg *.jpeg *.gif *.webp *.bmp *.tiff "
1085
+ "*.pdf *.docx *.xlsx *.pptx *.txt *.md *.csv *.tsv *.json);;"
1086
+ "Images (*.png *.jpg *.jpeg *.gif *.webp *.bmp *.tiff);;"
1087
+ "Documents (*.pdf *.docx *.xlsx *.pptx *.txt *.md);;"
1088
+ "Data files (*.csv *.tsv *.json);;"
1089
+ "All files (*.*)"
1090
+ )
1091
+
1092
+ if file_dialog.exec():
1093
+ selected_files = file_dialog.selectedFiles()
1094
+ for file_path in selected_files:
1095
+ if file_path not in self.attached_files:
1096
+ self.attached_files.append(file_path)
1097
+ if self.debug:
1098
+ print(f"📎 Attached file: {file_path}")
1099
+
1100
+ self.update_attached_files_display()
1101
+
1102
+ def update_attached_files_display(self):
1103
+ """Update the visual display of attached files."""
1104
+ # Clear existing file chips
1105
+ while self.attached_files_layout.count():
1106
+ child = self.attached_files_layout.takeAt(0)
1107
+ if child.widget():
1108
+ child.widget().deleteLater()
1109
+
1110
+ if not self.attached_files:
1111
+ self.attached_files_container.hide()
1112
+ return
1113
+
1114
+ # Show container and add file chips
1115
+ self.attached_files_container.show()
1116
+
1117
+ for file_path in self.attached_files:
1118
+ import os
1119
+ file_name = os.path.basename(file_path)
1120
+
1121
+ # Create file chip
1122
+ file_chip = QFrame()
1123
+ file_chip.setStyleSheet("""
1124
+ QFrame {
1125
+ background: rgba(0, 102, 204, 0.2);
1126
+ border: 1px solid rgba(0, 102, 204, 0.4);
1127
+ border-radius: 10px;
1128
+ padding: 2px 8px;
1129
+ }
1130
+ """)
1131
+
1132
+ chip_layout = QHBoxLayout(file_chip)
1133
+ chip_layout.setContentsMargins(4, 2, 4, 2)
1134
+ chip_layout.setSpacing(4)
1135
+
1136
+ # File icon based on type
1137
+ ext = os.path.splitext(file_name)[1].lower()
1138
+ if ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff']:
1139
+ icon = "🖼️"
1140
+ elif ext == '.pdf':
1141
+ icon = "📄"
1142
+ elif ext in ['.docx', '.doc']:
1143
+ icon = "📝"
1144
+ elif ext in ['.xlsx', '.xls']:
1145
+ icon = "📊"
1146
+ elif ext in ['.pptx', '.ppt']:
1147
+ icon = "📊"
1148
+ elif ext in ['.csv', '.tsv']:
1149
+ icon = "📋"
1150
+ else:
1151
+ icon = "📎"
1152
+
1153
+ file_label = QLabel(f"{icon} {file_name[:20]}{'...' if len(file_name) > 20 else ''}")
1154
+ file_label.setStyleSheet("background: transparent; border: none; color: rgba(255, 255, 255, 0.9); font-size: 10px;")
1155
+ chip_layout.addWidget(file_label)
1156
+
1157
+ # Remove button
1158
+ remove_btn = QPushButton("✕")
1159
+ remove_btn.setFixedSize(16, 16)
1160
+ remove_btn.setStyleSheet("""
1161
+ QPushButton {
1162
+ background: transparent;
1163
+ border: none;
1164
+ color: rgba(255, 255, 255, 0.6);
1165
+ font-size: 10px;
1166
+ padding: 0px;
1167
+ }
1168
+ QPushButton:hover {
1169
+ color: rgba(255, 60, 60, 0.9);
1170
+ }
1171
+ """)
1172
+ remove_btn.clicked.connect(lambda checked, fp=file_path: self.remove_attached_file(fp))
1173
+ chip_layout.addWidget(remove_btn)
1174
+
1175
+ self.attached_files_layout.addWidget(file_chip)
1176
+
1177
+ self.attached_files_layout.addStretch()
1178
+
1179
+ def remove_attached_file(self, file_path):
1180
+ """Remove a file from the attached files list."""
1181
+ if file_path in self.attached_files:
1182
+ self.attached_files.remove(file_path)
1183
+ if self.debug:
1184
+ print(f"🗑️ Removed attached file: {file_path}")
1185
+ self.update_attached_files_display()
1186
+
1026
1187
  def send_message(self):
1027
- """Send message to LLM."""
1188
+ """Send message to LLM with optional media attachments."""
1028
1189
  message = self.input_text.toPlainText().strip()
1029
1190
  if not message:
1030
1191
  return
1031
-
1192
+
1032
1193
  if self.debug:
1033
1194
  print(f"💬 Sending message: '{message[:50]}...' to {self.current_provider}/{self.current_model}")
1034
-
1195
+ if self.attached_files:
1196
+ print(f"📎 With {len(self.attached_files)} attached file(s)")
1197
+
1035
1198
  # 1. Clear input immediately
1036
1199
  self.input_text.clear()
1037
-
1038
- # 2. Don't manually add to history - let AbstractCore handle it via session.generate()
1039
- # The LLMManager will automatically add the message when generate_response is called
1040
-
1041
- # 3. Update UI for sending state
1200
+
1201
+ # 2. Capture attached files before clearing
1202
+ media_files = self.attached_files.copy()
1203
+
1204
+ # 3. Clear attached files display
1205
+ self.attached_files.clear()
1206
+ self.update_attached_files_display()
1207
+
1208
+ # 4. Update UI for sending state
1042
1209
  self.send_button.setEnabled(False)
1043
1210
  self.send_button.setText("⏳")
1044
1211
  self.status_label.setText("generating")
@@ -1056,21 +1223,27 @@ class QtChatBubble(QWidget):
1056
1223
  color: #fab387;
1057
1224
  }
1058
1225
  """)
1059
-
1226
+
1060
1227
  # Notify main app about status change (for icon animation)
1061
1228
  if self.status_callback:
1062
1229
  self.status_callback("generating")
1063
-
1230
+
1064
1231
  print("🔄 QtChatBubble: UI updated, creating worker thread...")
1065
-
1066
- # 4. Start worker thread to send request
1067
- self.worker = LLMWorker(self.llm_manager, message, self.current_provider, self.current_model)
1232
+
1233
+ # 5. Start worker thread to send request with optional media files
1234
+ self.worker = LLMWorker(
1235
+ self.llm_manager,
1236
+ message,
1237
+ self.current_provider,
1238
+ self.current_model,
1239
+ media=media_files if media_files else None
1240
+ )
1068
1241
  self.worker.response_ready.connect(self.on_response_ready)
1069
1242
  self.worker.error_occurred.connect(self.on_error_occurred)
1070
-
1243
+
1071
1244
  print("🔄 QtChatBubble: Starting worker thread...")
1072
1245
  self.worker.start()
1073
-
1246
+
1074
1247
  print("🔄 QtChatBubble: Worker thread started, hiding bubble...")
1075
1248
  # Hide bubble after sending (like the original design)
1076
1249
  QTimer.singleShot(500, self.hide)
@@ -1450,7 +1623,7 @@ class QtChatBubble(QWidget):
1450
1623
  font-size: 10px;
1451
1624
  font-weight: 600;
1452
1625
  color: #ffffff;
1453
- font-family: -apple-system, system-ui, sans-serif;
1626
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
1454
1627
  }}
1455
1628
  """)
1456
1629
 
@@ -1952,7 +2125,7 @@ class QtChatBubble(QWidget):
1952
2125
  border-radius: 11px;
1953
2126
  font-size: 10px;
1954
2127
  color: #ffffff;
1955
- font-family: -apple-system, system-ui, sans-serif;
2128
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
1956
2129
  padding: 0 10px;
1957
2130
  font-weight: 600;
1958
2131
  }
@@ -1969,7 +2142,7 @@ class QtChatBubble(QWidget):
1969
2142
  border-radius: 11px;
1970
2143
  font-size: 10px;
1971
2144
  color: rgba(255, 255, 255, 0.7);
1972
- font-family: -apple-system, system-ui, sans-serif;
2145
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
1973
2146
  padding: 0 10px;
1974
2147
  }
1975
2148
  QPushButton:hover {
@@ -107,7 +107,7 @@ class ToastWindow(QWidget):
107
107
  color: rgba(255, 255, 255, 0.9);
108
108
  background: transparent;
109
109
  border: none;
110
- font-family: -apple-system, system-ui, sans-serif;
110
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
111
111
  }
112
112
  """)
113
113
  header_layout.addWidget(title_label)
@@ -128,7 +128,7 @@ class ToastWindow(QWidget):
128
128
  border-radius: 12px;
129
129
  font-size: 11px;
130
130
  color: rgba(255, 255, 255, 0.7);
131
- font-family: -apple-system, system-ui, sans-serif;
131
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
132
132
  }
133
133
  QPushButton:hover {
134
134
  background: rgba(255, 255, 255, 0.15);
@@ -149,7 +149,7 @@ class ToastWindow(QWidget):
149
149
  border-radius: 12px;
150
150
  font-size: 11px;
151
151
  color: rgba(255, 255, 255, 0.7);
152
- font-family: -apple-system, system-ui, sans-serif;
152
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
153
153
  }
154
154
  QPushButton:hover {
155
155
  background: rgba(255, 255, 255, 0.15);
@@ -173,7 +173,7 @@ class ToastWindow(QWidget):
173
173
  border-radius: 12px;
174
174
  font-size: 11px;
175
175
  color: rgba(255, 255, 255, 0.7);
176
- font-family: -apple-system, system-ui, sans-serif;
176
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
177
177
  }
178
178
  QPushButton:hover {
179
179
  background: rgba(255, 255, 255, 0.15);
@@ -194,7 +194,7 @@ class ToastWindow(QWidget):
194
194
  border-radius: 12px;
195
195
  font-size: 11px;
196
196
  color: rgba(255, 255, 255, 0.7);
197
- font-family: -apple-system, system-ui, sans-serif;
197
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
198
198
  }
199
199
  QPushButton:hover {
200
200
  background: rgba(255, 255, 255, 0.15);
@@ -260,7 +260,7 @@ class ToastWindow(QWidget):
260
260
  color: rgba(255, 255, 255, 0.9);
261
261
  background: transparent;
262
262
  border: none;
263
- font-family: -apple-system, system-ui, sans-serif;
263
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
264
264
  font-size: 11px;
265
265
  font-weight: 500;
266
266
  }
@@ -274,7 +274,7 @@ class ToastWindow(QWidget):
274
274
  font-size: 10px;
275
275
  font-weight: 500;
276
276
  color: rgba(255, 255, 255, 0.8);
277
- font-family: -apple-system, system-ui, sans-serif;
277
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
278
278
  }
279
279
 
280
280
  QPushButton:hover {
@@ -295,7 +295,7 @@ class ToastWindow(QWidget):
295
295
  font-size: 13px;
296
296
  font-weight: 400;
297
297
  color: rgba(255, 255, 255, 0.95);
298
- font-family: -apple-system, system-ui, sans-serif;
298
+ font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
299
299
  selection-background-color: rgba(34, 197, 94, 0.3);
300
300
  line-height: 1.5;
301
301
  }
@@ -276,7 +276,7 @@ class UIStyles:
276
276
  padding: 8px;
277
277
  background: {COLORS['surface']};
278
278
  font-size: 13px;
279
- font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
279
+ font-family: 'SF Pro Text', "Helvetica Neue", BlinkMacSystemFont, sans-serif;
280
280
  }}
281
281
  QTextEdit:focus {{
282
282
  border-color: {COLORS['primary']};
@@ -291,7 +291,7 @@ class UIStyles:
291
291
  padding: 12px 16px;
292
292
  background: {COLORS['surface']};
293
293
  font-size: 14px;
294
- font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
294
+ font-family: 'SF Pro Text', "Helvetica Neue", BlinkMacSystemFont, sans-serif;
295
295
  max-height: 120px;
296
296
  min-height: 40px;
297
297
  }}
@@ -103,7 +103,7 @@ class MarkdownRenderer:
103
103
  """Get base CSS styles for markdown content."""
104
104
  return """
105
105
  .markdown-content {
106
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
106
+ font-family: "SF Pro Text", "Helvetica Neue", BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
107
107
  font-size: 14px; /* Base font size */
108
108
  line-height: 1.6;
109
109
  color: #e2e8f0;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractassistant
3
- Version: 0.2.0
3
+ Version: 0.2.5
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
6
  License: MIT
@@ -22,12 +22,13 @@ Classifier: Topic :: Desktop Environment
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: abstractcore[all]>=2.4.2
25
+ Requires-Dist: abstractcore[all]>=2.4.5
26
26
  Requires-Dist: pystray>=0.19.4
27
27
  Requires-Dist: Pillow>=10.0.0
28
28
  Requires-Dist: PyQt5>=5.15.0
29
29
  Requires-Dist: markdown>=3.5.0
30
30
  Requires-Dist: pygments>=2.16.0
31
+ Requires-Dist: pymdown-extensions>=10.0
31
32
  Requires-Dist: abstractvoice>=0.5.0
32
33
  Requires-Dist: pyperclip>=1.8.2
33
34
  Requires-Dist: plyer>=2.1.0
@@ -1,9 +1,10 @@
1
- abstractcore[all]>=2.4.2
1
+ abstractcore[all]>=2.4.5
2
2
  pystray>=0.19.4
3
3
  Pillow>=10.0.0
4
4
  PyQt5>=5.15.0
5
5
  markdown>=3.5.0
6
6
  pygments>=2.16.0
7
+ pymdown-extensions>=10.0
7
8
  abstractvoice>=0.5.0
8
9
  pyperclip>=1.8.2
9
10
  plyer>=2.1.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "abstractassistant"
7
- version = "0.2.0"
7
+ version = "0.2.5"
8
8
  description = "A sleek (macOS) system tray application providing instant access to LLMs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -28,12 +28,13 @@ classifiers = [
28
28
  ]
29
29
 
30
30
  dependencies = [
31
- "abstractcore[all]>=2.4.2",
31
+ "abstractcore[all]>=2.4.5",
32
32
  "pystray>=0.19.4",
33
33
  "Pillow>=10.0.0",
34
34
  "PyQt5>=5.15.0",
35
35
  "markdown>=3.5.0",
36
36
  "pygments>=2.16.0",
37
+ "pymdown-extensions>=10.0",
37
38
  "abstractvoice>=0.5.0",
38
39
  "pyperclip>=1.8.2",
39
40
  "plyer>=2.1.0",