abstractassistant 0.2.6__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractassistant/app.py +290 -179
- abstractassistant/core/llm_manager.py +189 -19
- abstractassistant/core/tts_manager.py +75 -25
- abstractassistant/create_app_bundle.py +6 -1
- abstractassistant/ui/history_dialog.py +5 -5
- abstractassistant/ui/qt_bubble.py +291 -121
- abstractassistant/ui/toast_window.py +14 -15
- abstractassistant/ui/ui_styles.py +2 -2
- abstractassistant/utils/icon_generator.py +269 -139
- abstractassistant/utils/markdown_renderer.py +1 -1
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/METADATA +13 -9
- abstractassistant-0.3.0.dist-info/RECORD +28 -0
- setup_macos_app.py +64 -10
- abstractassistant-0.2.6.dist-info/RECORD +0 -28
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/WHEEL +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -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: "
|
|
110
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
131
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
152
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
176
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
197
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
263
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
277
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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: "
|
|
298
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
299
299
|
selection-background-color: rgba(34, 197, 94, 0.3);
|
|
300
300
|
line-height: 1.5;
|
|
301
301
|
}
|
|
@@ -506,11 +506,10 @@ class ToastManager:
|
|
|
506
506
|
self.debug = debug
|
|
507
507
|
self.current_toast: Optional[ToastWindow] = None
|
|
508
508
|
|
|
509
|
-
#
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
self.app = QApplication.instance()
|
|
509
|
+
# Always use existing QApplication instance (never create a new one)
|
|
510
|
+
self.app = QApplication.instance()
|
|
511
|
+
if not self.app:
|
|
512
|
+
raise RuntimeError("No QApplication instance found. This should be created by the main app first.")
|
|
514
513
|
|
|
515
514
|
if self.debug:
|
|
516
515
|
print("✅ ToastManager initialized")
|
|
@@ -546,10 +545,10 @@ _active_toasts = []
|
|
|
546
545
|
def show_toast_notification(message: str, debug: bool = False, voice_manager=None):
|
|
547
546
|
"""Standalone function to show a toast notification - stays visible until manually closed."""
|
|
548
547
|
try:
|
|
549
|
-
#
|
|
548
|
+
# Always use existing QApplication instance (never create a new one)
|
|
550
549
|
app = QApplication.instance()
|
|
551
550
|
if not app:
|
|
552
|
-
|
|
551
|
+
raise RuntimeError("No QApplication instance found. This should be created by the main app first.")
|
|
553
552
|
|
|
554
553
|
# Create and show toast (no auto-hide)
|
|
555
554
|
toast = ToastWindow(message, debug=debug, voice_manager=voice_manager)
|
|
@@ -276,7 +276,7 @@ class UIStyles:
|
|
|
276
276
|
padding: 8px;
|
|
277
277
|
background: {COLORS['surface']};
|
|
278
278
|
font-size: 13px;
|
|
279
|
-
font-family: '
|
|
279
|
+
font-family: 'Helvetica Neue', "Helvetica", Arial, 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: '
|
|
294
|
+
font-family: 'Helvetica Neue', "Helvetica", Arial, sans-serif;
|
|
295
295
|
max-height: 120px;
|
|
296
296
|
min-height: 40px;
|
|
297
297
|
}}
|
|
@@ -49,7 +49,7 @@ class IconGenerator:
|
|
|
49
49
|
return img
|
|
50
50
|
|
|
51
51
|
def _draw_gradient_circle(self, draw: ImageDraw.Draw, center: int, radius: int, color_scheme: str = "blue", animated: bool = False):
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Color schemes - more vibrant and visible
|
|
54
54
|
colors = {
|
|
55
55
|
"blue": (64, 150, 255), # Brighter blue
|
|
@@ -87,172 +87,302 @@ class IconGenerator:
|
|
|
87
87
|
intensity = 0.2
|
|
88
88
|
|
|
89
89
|
elif color_scheme == "green":
|
|
90
|
-
# Ready state: much more visible heartbeat
|
|
91
90
|
base_color = colors["green"]
|
|
92
91
|
if animated:
|
|
93
92
|
import time
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
if pulse_cycle < 0.2: # Strong beat
|
|
97
|
-
intensity = 2.0 # Much brighter
|
|
98
|
-
elif pulse_cycle < 0.4: # Fade down
|
|
99
|
-
intensity = 1.2
|
|
100
|
-
else: # Rest period - much dimmer
|
|
101
|
-
intensity = 0.4 # Much darker for contrast
|
|
93
|
+
# Gentle breathing for ready state
|
|
94
|
+
intensity = 0.8 + 0.4 * math.sin(time.time() * 0.5) # Slower breathing
|
|
102
95
|
else:
|
|
103
96
|
intensity = 1.0
|
|
104
97
|
else:
|
|
105
98
|
base_color = colors.get(color_scheme, colors["blue"])
|
|
106
99
|
intensity = 1.0
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
alpha = int(255 * gradient_factor * intensity)
|
|
117
|
-
|
|
118
|
-
# Ensure alpha is within bounds
|
|
119
|
-
alpha = max(0, min(255, alpha))
|
|
120
|
-
|
|
121
|
-
# Create more vibrant color with better gradient
|
|
122
|
-
color = (*base_color, alpha)
|
|
123
|
-
draw.ellipse(
|
|
124
|
-
[center - radius + i, center - radius + i,
|
|
125
|
-
center + radius - i, center + radius - i],
|
|
126
|
-
fill=color
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Add bright center core for more dramatic effect
|
|
130
|
-
core_radius = max(1, radius // 4)
|
|
131
|
-
core_alpha = int(255 * intensity * 1.2) # Extra bright center
|
|
132
|
-
core_alpha = max(0, min(255, core_alpha))
|
|
133
|
-
core_color = (*base_color, core_alpha)
|
|
134
|
-
draw.ellipse(
|
|
135
|
-
[center - core_radius, center - core_radius,
|
|
136
|
-
center + core_radius, center + core_radius],
|
|
137
|
-
fill=core_color
|
|
138
|
-
)
|
|
100
|
+
|
|
101
|
+
# Apply intensity to color
|
|
102
|
+
final_color = tuple(int(c * intensity) for c in base_color)
|
|
103
|
+
|
|
104
|
+
# Draw main circle with gradient effect
|
|
105
|
+
for i in range(radius, 0, -2):
|
|
106
|
+
alpha = int(255 * (i / radius) * 0.8)
|
|
107
|
+
circle_color = final_color + (alpha,)
|
|
108
|
+
draw.ellipse([center-i, center-i, center+i, center+i], fill=circle_color)
|
|
139
109
|
|
|
140
110
|
def _draw_neural_nodes(self, draw: ImageDraw.Draw, center: int, radius: int, animated: bool = False):
|
|
141
|
-
"""Draw neural network
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Surrounding nodes
|
|
160
|
-
num_nodes = 6
|
|
161
|
-
outer_radius = int(radius * 0.7)
|
|
162
|
-
|
|
163
|
-
for i in range(num_nodes):
|
|
164
|
-
angle = (2 * math.pi * i) / num_nodes
|
|
165
|
-
x = center + int(outer_radius * math.cos(angle))
|
|
166
|
-
y = center + int(outer_radius * math.sin(angle))
|
|
111
|
+
"""Draw neural network nodes around the circle."""
|
|
112
|
+
node_positions = [
|
|
113
|
+
(center + radius * 0.6, center - radius * 0.3),
|
|
114
|
+
(center + radius * 0.3, center + radius * 0.6),
|
|
115
|
+
(center - radius * 0.4, center + radius * 0.4),
|
|
116
|
+
(center - radius * 0.6, center - radius * 0.2),
|
|
117
|
+
(center - radius * 0.1, center - radius * 0.7)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
for i, (x, y) in enumerate(node_positions):
|
|
121
|
+
node_radius = 3 + (i % 2) # Varying sizes
|
|
122
|
+
if animated:
|
|
123
|
+
import time
|
|
124
|
+
# Subtle pulsing
|
|
125
|
+
pulse = 1 + 0.3 * math.sin(time.time() * 2 + i)
|
|
126
|
+
node_radius *= pulse
|
|
167
127
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
[x - small_radius, y - small_radius,
|
|
171
|
-
x + small_radius, y + small_radius],
|
|
172
|
-
fill=(255, 255, 255, small_node_alpha)
|
|
173
|
-
)
|
|
128
|
+
draw.ellipse([x-node_radius, y-node_radius, x+node_radius, y+node_radius],
|
|
129
|
+
fill=(255, 255, 255, 180))
|
|
174
130
|
|
|
175
131
|
def _draw_neural_connections(self, draw: ImageDraw.Draw, center: int, radius: int, animated: bool = False):
|
|
176
|
-
"""Draw
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if animated:
|
|
184
|
-
import time
|
|
185
|
-
pulse = abs(math.sin(time.time() * 2.5)) * 0.3 + 0.7 # Pulse between 0.7 and 1.0
|
|
186
|
-
line_alpha = int(line_alpha * pulse)
|
|
187
|
-
connection_alpha = int(connection_alpha * pulse)
|
|
188
|
-
|
|
189
|
-
# Draw lines from center to outer nodes
|
|
190
|
-
for i in range(num_nodes):
|
|
191
|
-
angle = (2 * math.pi * i) / num_nodes
|
|
192
|
-
x = center + int(outer_radius * math.cos(angle))
|
|
193
|
-
y = center + int(outer_radius * math.sin(angle))
|
|
194
|
-
|
|
195
|
-
draw.line(
|
|
196
|
-
[center, center, x, y],
|
|
197
|
-
fill=(255, 255, 255, line_alpha),
|
|
198
|
-
width=2
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# Draw some connections between outer nodes
|
|
202
|
-
for i in range(0, num_nodes, 2):
|
|
203
|
-
angle1 = (2 * math.pi * i) / num_nodes
|
|
204
|
-
angle2 = (2 * math.pi * ((i + 2) % num_nodes)) / num_nodes
|
|
205
|
-
|
|
206
|
-
x1 = center + int(outer_radius * math.cos(angle1))
|
|
207
|
-
y1 = center + int(outer_radius * math.sin(angle1))
|
|
208
|
-
x2 = center + int(outer_radius * math.cos(angle2))
|
|
209
|
-
y2 = center + int(outer_radius * math.sin(angle2))
|
|
210
|
-
|
|
211
|
-
draw.line(
|
|
212
|
-
[x1, y1, x2, y2],
|
|
213
|
-
fill=(255, 255, 255, connection_alpha),
|
|
214
|
-
width=1
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
def _add_glow_effect(self, img: Image.Image, color_scheme: str = "blue") -> Image.Image:
|
|
218
|
-
"""Add a subtle glow effect to the icon."""
|
|
219
|
-
# Create a slightly larger version for the glow
|
|
220
|
-
glow_size = self.size + 8
|
|
221
|
-
glow_img = Image.new('RGBA', (glow_size, glow_size), (0, 0, 0, 0))
|
|
132
|
+
"""Draw connecting lines between nodes."""
|
|
133
|
+
connections = [
|
|
134
|
+
((center + radius * 0.6, center - radius * 0.3), (center + radius * 0.3, center + radius * 0.6)),
|
|
135
|
+
((center + radius * 0.3, center + radius * 0.6), (center - radius * 0.4, center + radius * 0.4)),
|
|
136
|
+
((center - radius * 0.4, center + radius * 0.4), (center - radius * 0.6, center - radius * 0.2)),
|
|
137
|
+
((center - radius * 0.1, center - radius * 0.7), (center + radius * 0.6, center - radius * 0.3))
|
|
138
|
+
]
|
|
222
139
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
140
|
+
for (x1, y1), (x2, y2) in connections:
|
|
141
|
+
draw.line([(x1, y1), (x2, y2)], fill=(255, 255, 255, 120), width=2)
|
|
142
|
+
|
|
143
|
+
def _add_glow_effect(self, img: Image.Image, color_scheme: str) -> Image.Image:
|
|
144
|
+
"""Add a subtle glow effect around the icon."""
|
|
145
|
+
# Create glow layer
|
|
146
|
+
glow = img.filter(ImageFilter.GaussianBlur(radius=3))
|
|
229
147
|
|
|
230
|
-
#
|
|
231
|
-
|
|
148
|
+
# Composite original on top of glow
|
|
149
|
+
result = Image.alpha_composite(glow, img)
|
|
150
|
+
return result
|
|
232
151
|
|
|
233
152
|
def create_status_icon(self, status: str) -> Image.Image:
|
|
234
|
-
"""Create a status indicator icon.
|
|
153
|
+
"""Create a simple status indicator icon.
|
|
235
154
|
|
|
236
155
|
Args:
|
|
237
|
-
status: Status
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
Small status icon image
|
|
156
|
+
status: Status type ('ready', 'working', 'error', 'warning')
|
|
241
157
|
"""
|
|
242
|
-
size =
|
|
158
|
+
size = self.size
|
|
243
159
|
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
|
244
160
|
draw = ImageDraw.Draw(img)
|
|
245
161
|
|
|
162
|
+
# Status colors
|
|
246
163
|
colors = {
|
|
247
|
-
'ready': (
|
|
248
|
-
'
|
|
249
|
-
'
|
|
250
|
-
'
|
|
164
|
+
'ready': (52, 199, 89), # Green
|
|
165
|
+
'working': (255, 149, 0), # Orange
|
|
166
|
+
'error': (255, 59, 48), # Red
|
|
167
|
+
'warning': (255, 204, 0), # Yellow
|
|
168
|
+
'thinking': (175, 82, 222), # Purple
|
|
169
|
+
'speaking': (0, 122, 255) # Blue
|
|
251
170
|
}
|
|
252
171
|
|
|
253
|
-
color = colors.get(status,
|
|
172
|
+
color = colors.get(status, colors['ready'])
|
|
254
173
|
|
|
255
174
|
# Draw status circle
|
|
256
175
|
draw.ellipse([2, 2, size-2, size-2], fill=color)
|
|
257
176
|
|
|
258
177
|
return img
|
|
178
|
+
|
|
179
|
+
def apply_heartbeat_effect(self, base_icon: Image.Image, status: str = "ready") -> Image.Image:
|
|
180
|
+
"""Apply DRAMATIC animated effect with solid colors and rotating elements.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
base_icon: Base icon image to apply effect to
|
|
184
|
+
status: Status for animation type ('ready', 'thinking', 'speaking')
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Icon with dramatic animated effect applied
|
|
188
|
+
"""
|
|
189
|
+
import time
|
|
190
|
+
import math
|
|
191
|
+
from PIL import ImageFilter, ImageEnhance, ImageDraw
|
|
192
|
+
|
|
193
|
+
# Debug: Print status occasionally (only in debug mode)
|
|
194
|
+
if hasattr(self, 'debug') and self.debug:
|
|
195
|
+
if hasattr(self, '_last_debug_time'):
|
|
196
|
+
if time.time() - self._last_debug_time > 3: # Every 3 seconds
|
|
197
|
+
print(f"🎨 Icon animation status: {status}")
|
|
198
|
+
self._last_debug_time = time.time()
|
|
199
|
+
else:
|
|
200
|
+
print(f"🎨 Icon animation status: {status}")
|
|
201
|
+
self._last_debug_time = time.time()
|
|
202
|
+
|
|
203
|
+
# Print status changes only in debug mode
|
|
204
|
+
if not hasattr(self, '_last_status') or self._last_status != status:
|
|
205
|
+
if hasattr(self, 'debug') and self.debug:
|
|
206
|
+
print(f"🔄 Icon status changed: {getattr(self, '_last_status', 'none')} → {status}")
|
|
207
|
+
self._last_status = status
|
|
208
|
+
|
|
209
|
+
# SOLID background colors for maximum visibility
|
|
210
|
+
solid_colors = {
|
|
211
|
+
'ready': (0, 255, 80), # Bright green
|
|
212
|
+
'thinking': (255, 60, 100), # Bright red
|
|
213
|
+
'speaking': (60, 150, 255), # Bright blue
|
|
214
|
+
'generating': (255, 160, 0) # Bright orange
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Create a new dramatic icon instead of modifying the base
|
|
218
|
+
size = base_icon.size[0]
|
|
219
|
+
center = size // 2
|
|
220
|
+
|
|
221
|
+
# Create new image with transparent background
|
|
222
|
+
result = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
|
223
|
+
draw = ImageDraw.Draw(result)
|
|
224
|
+
|
|
225
|
+
# Get current time for animation
|
|
226
|
+
current_time = time.time()
|
|
227
|
+
|
|
228
|
+
# Get base color for this status
|
|
229
|
+
base_color = solid_colors.get(status, solid_colors['ready'])
|
|
230
|
+
|
|
231
|
+
# Status-specific animation patterns with rotation
|
|
232
|
+
# Debug output disabled for clean terminal
|
|
233
|
+
# print(f"🎯 Animation logic: status='{status}', base_color={base_color}")
|
|
234
|
+
|
|
235
|
+
if status == 'thinking':
|
|
236
|
+
# print("🔴 THINKING: Drawing rotating red bars") # Debug disabled
|
|
237
|
+
# Fast rotating bars with red color - CLOCKWISE rotation
|
|
238
|
+
rotation_speed = 4.0 # 4 rotations per second
|
|
239
|
+
angle = -(current_time * rotation_speed * 360) % 360 # Negative for clockwise
|
|
240
|
+
|
|
241
|
+
# Double-heartbeat intensity
|
|
242
|
+
heartbeat = (current_time * 3.0) % 1 # 3Hz heartbeat
|
|
243
|
+
if heartbeat < 0.1:
|
|
244
|
+
intensity = 1.0
|
|
245
|
+
elif heartbeat < 0.2:
|
|
246
|
+
intensity = 0.3
|
|
247
|
+
elif heartbeat < 0.3:
|
|
248
|
+
intensity = 1.2
|
|
249
|
+
else:
|
|
250
|
+
intensity = 0.2
|
|
251
|
+
|
|
252
|
+
# Draw rotating bars
|
|
253
|
+
self._draw_rotating_bars(draw, center, size, angle, base_color, intensity)
|
|
254
|
+
|
|
255
|
+
elif status == 'speaking':
|
|
256
|
+
# print("🔵 SPEAKING: Drawing vibrating blue bars") # Debug disabled
|
|
257
|
+
# print(f"🔵 SPEAKING: Using color {base_color} (should be blue)") # Debug disabled
|
|
258
|
+
|
|
259
|
+
# Create voice frequency-like vibration pattern
|
|
260
|
+
freq1 = 8.0 # High frequency vibration
|
|
261
|
+
freq2 = 3.0 # Medium frequency modulation
|
|
262
|
+
freq3 = 1.5 # Low frequency envelope
|
|
263
|
+
|
|
264
|
+
# Complex vibration pattern mimicking voice
|
|
265
|
+
vibration = (math.sin(current_time * freq1 * 2 * math.pi) * 0.3 +
|
|
266
|
+
math.sin(current_time * freq2 * 2 * math.pi) * 0.4 +
|
|
267
|
+
math.sin(current_time * freq3 * 2 * math.pi) * 0.3)
|
|
268
|
+
intensity = 0.7 + vibration * 0.3
|
|
269
|
+
|
|
270
|
+
# Draw vibrating voice bars (vertical bars that vibrate)
|
|
271
|
+
self._draw_voice_bars(draw, center, size, base_color, intensity, current_time)
|
|
272
|
+
|
|
273
|
+
elif status == 'ready':
|
|
274
|
+
# print("🟢 READY: Drawing breathing green circle") # Debug disabled
|
|
275
|
+
# Slow breathing circle with green color
|
|
276
|
+
breath = 0.5 + 0.5 * math.sin(current_time * 0.6 * math.pi) # 0.3Hz breathing
|
|
277
|
+
intensity = 0.4 + breath * 0.3
|
|
278
|
+
|
|
279
|
+
# Draw breathing circle (no rotation)
|
|
280
|
+
self._draw_breathing_circle(draw, center, size, base_color, intensity)
|
|
281
|
+
|
|
282
|
+
else:
|
|
283
|
+
# print(f"❓ UNKNOWN STATUS: '{status}' - using default circle") # Debug disabled
|
|
284
|
+
# Default: static circle
|
|
285
|
+
self._draw_breathing_circle(draw, center, size, base_color, 0.5)
|
|
286
|
+
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
def _draw_rotating_bars(self, draw, center, size, angle, color, intensity):
|
|
290
|
+
"""Draw rotating bars for thinking status."""
|
|
291
|
+
# Adjust color intensity
|
|
292
|
+
r, g, b = color
|
|
293
|
+
r = int(min(255, r * intensity))
|
|
294
|
+
g = int(min(255, g * intensity))
|
|
295
|
+
b = int(min(255, b * intensity))
|
|
296
|
+
bar_color = (r, g, b, 255)
|
|
297
|
+
|
|
298
|
+
# Draw 4 bars rotating around center
|
|
299
|
+
bar_length = size * 0.3
|
|
300
|
+
bar_width = size * 0.08
|
|
301
|
+
|
|
302
|
+
for i in range(4):
|
|
303
|
+
bar_angle = angle + (i * 90)
|
|
304
|
+
rad = math.radians(bar_angle)
|
|
305
|
+
|
|
306
|
+
# Calculate bar endpoints
|
|
307
|
+
start_x = center + math.cos(rad) * (size * 0.15)
|
|
308
|
+
start_y = center + math.sin(rad) * (size * 0.15)
|
|
309
|
+
end_x = center + math.cos(rad) * (size * 0.35)
|
|
310
|
+
end_y = center + math.sin(rad) * (size * 0.35)
|
|
311
|
+
|
|
312
|
+
# Draw thick line as bar
|
|
313
|
+
self._draw_thick_line(draw, start_x, start_y, end_x, end_y, bar_width, bar_color)
|
|
314
|
+
|
|
315
|
+
def _draw_voice_bars(self, draw, center, size, color, intensity, current_time):
|
|
316
|
+
"""Draw vibrating voice bars for speaking status."""
|
|
317
|
+
import math
|
|
318
|
+
|
|
319
|
+
# Adjust color intensity
|
|
320
|
+
r, g, b = color
|
|
321
|
+
r = int(min(255, r * intensity))
|
|
322
|
+
g = int(min(255, g * intensity))
|
|
323
|
+
b = int(min(255, b * intensity))
|
|
324
|
+
bar_color = (r, g, b, 255)
|
|
325
|
+
|
|
326
|
+
# Draw 5 vertical bars with different vibration frequencies (like voice visualizer)
|
|
327
|
+
bar_count = 5
|
|
328
|
+
bar_width = size * 0.08
|
|
329
|
+
bar_spacing = size * 0.12
|
|
330
|
+
|
|
331
|
+
for i in range(bar_count):
|
|
332
|
+
# Each bar has slightly different frequency for realistic voice effect
|
|
333
|
+
bar_freq = 6.0 + i * 1.5 # Different frequencies per bar
|
|
334
|
+
bar_vibration = math.sin(current_time * bar_freq * 2 * math.pi)
|
|
335
|
+
|
|
336
|
+
# Bar height varies with vibration (like audio visualizer)
|
|
337
|
+
base_height = size * 0.15
|
|
338
|
+
vibration_height = size * 0.25 * abs(bar_vibration)
|
|
339
|
+
total_height = base_height + vibration_height
|
|
340
|
+
|
|
341
|
+
# Position bars horizontally across the icon
|
|
342
|
+
x = center - (bar_count - 1) * bar_spacing / 2 + i * bar_spacing
|
|
343
|
+
y_top = center - total_height / 2
|
|
344
|
+
y_bottom = center + total_height / 2
|
|
345
|
+
|
|
346
|
+
# Draw vertical bar
|
|
347
|
+
bbox = [x - bar_width/2, y_top, x + bar_width/2, y_bottom]
|
|
348
|
+
draw.rectangle(bbox, fill=bar_color)
|
|
349
|
+
|
|
350
|
+
def _draw_breathing_circle(self, draw, center, size, color, intensity):
|
|
351
|
+
"""Draw breathing circle for ready status."""
|
|
352
|
+
# Adjust color intensity
|
|
353
|
+
r, g, b = color
|
|
354
|
+
r = int(min(255, r * intensity))
|
|
355
|
+
g = int(min(255, g * intensity))
|
|
356
|
+
b = int(min(255, b * intensity))
|
|
357
|
+
circle_color = (r, g, b, 255)
|
|
358
|
+
|
|
359
|
+
# Draw MUCH LARGER pulsing circle to match menu bar icon size
|
|
360
|
+
base_radius = size * 0.35 # Much larger base size
|
|
361
|
+
radius = base_radius * (0.8 + 0.4 * intensity)
|
|
362
|
+
bbox = [center - radius, center - radius, center + radius, center + radius]
|
|
363
|
+
draw.ellipse(bbox, fill=circle_color)
|
|
364
|
+
|
|
365
|
+
def _draw_thick_line(self, draw, x1, y1, x2, y2, width, color):
|
|
366
|
+
"""Draw a thick line between two points."""
|
|
367
|
+
import math
|
|
368
|
+
# Calculate perpendicular offset for thickness
|
|
369
|
+
dx = x2 - x1
|
|
370
|
+
dy = y2 - y1
|
|
371
|
+
length = math.sqrt(dx*dx + dy*dy)
|
|
372
|
+
if length == 0:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
# Normalize and get perpendicular
|
|
376
|
+
dx /= length
|
|
377
|
+
dy /= length
|
|
378
|
+
px = -dy * width / 2
|
|
379
|
+
py = dx * width / 2
|
|
380
|
+
|
|
381
|
+
# Draw polygon for thick line
|
|
382
|
+
points = [
|
|
383
|
+
(x1 + px, y1 + py),
|
|
384
|
+
(x1 - px, y1 - py),
|
|
385
|
+
(x2 - px, y2 - py),
|
|
386
|
+
(x2 + px, y2 + py)
|
|
387
|
+
]
|
|
388
|
+
draw.polygon(points, fill=color)
|
|
@@ -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: "
|
|
106
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, 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.
|
|
3
|
+
Version: 0.3.0
|
|
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-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Desktop Environment
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: abstractcore[all]>=2.
|
|
24
|
+
Requires-Dist: abstractcore[all]>=2.5.0
|
|
25
25
|
Requires-Dist: pystray>=0.19.4
|
|
26
26
|
Requires-Dist: Pillow>=10.0.0
|
|
27
27
|
Requires-Dist: PyQt5>=5.15.0
|
|
@@ -69,18 +69,22 @@ A sleek macOS system tray application providing instant access to Large Language
|
|
|
69
69
|
|
|
70
70
|
#### 🍎 macOS Users (Recommended)
|
|
71
71
|
```bash
|
|
72
|
-
#
|
|
73
|
-
|
|
72
|
+
# Install AbstractAssistant
|
|
73
|
+
pip install abstractassistant
|
|
74
|
+
|
|
75
|
+
# Create native macOS app bundle
|
|
76
|
+
create-app-bundle
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
This will:
|
|
77
|
-
- Install AbstractAssistant from PyPI
|
|
78
|
-
- Create a macOS app bundle in `/Applications`
|
|
79
|
-
- Add AbstractAssistant to your Dock
|
|
80
|
+
- Install AbstractAssistant from PyPI with all dependencies
|
|
81
|
+
- Create a native macOS app bundle in `/Applications`
|
|
82
|
+
- Add AbstractAssistant to your Dock with a beautiful neural network icon
|
|
83
|
+
- Enable launch from Spotlight, Finder, and Dock
|
|
80
84
|
|
|
81
85
|
#### 🔧 Standard Installation
|
|
82
86
|
```bash
|
|
83
|
-
# Install from PyPI
|
|
87
|
+
# Install from PyPI (terminal access only)
|
|
84
88
|
pip install abstractassistant
|
|
85
89
|
```
|
|
86
90
|
|
|
@@ -98,7 +102,7 @@ For detailed installation instructions including prerequisites and voice setup,
|
|
|
98
102
|
# Launch the assistant
|
|
99
103
|
assistant
|
|
100
104
|
|
|
101
|
-
#
|
|
105
|
+
# Create macOS app bundle after installation
|
|
102
106
|
create-app-bundle
|
|
103
107
|
```
|
|
104
108
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
setup_macos_app.py,sha256=9dIPr9TipjtgdIhd0MnR2syRNoFyBVMnRsWDW0UCT3A,10736
|
|
2
|
+
abstractassistant/__init__.py,sha256=homfqMDh6sX2nBROtk6-y72jnrStPph8gEOeT0OjKyU,35
|
|
3
|
+
abstractassistant/app.py,sha256=9Dn_2ShT6O5sE3Qf09oh-wKQpLFzKTNVJzZbB0POOYI,40078
|
|
4
|
+
abstractassistant/cli.py,sha256=SQPxQCLjX-LOlhSEvG302D0AOyxlxo5QM2imxr9wxmc,4385
|
|
5
|
+
abstractassistant/config.py,sha256=KodfPYTpHtavJyne-h-B-r3kbEt1uusSY8GknGLtDL8,5809
|
|
6
|
+
abstractassistant/create_app_bundle.py,sha256=LAZdp2C90ikMVd3KPdwNYBYUASbHpypOJIwvx6fQyXM,1698
|
|
7
|
+
abstractassistant/web_server.py,sha256=_pqMzy13qfim9BMBqQJQifWyX7UQXFD_sZeiu4ZBt40,12816
|
|
8
|
+
abstractassistant/core/__init__.py,sha256=TETStgToTe7QSsCZgRHDk2oSErlLJoeGN0sFg4Yx2_c,15
|
|
9
|
+
abstractassistant/core/llm_manager.py,sha256=hJun-nDfRv9zxv_3tfrHAmVYSYT96E-0zDJB2TiaSeQ,19226
|
|
10
|
+
abstractassistant/core/tts_manager.py,sha256=Cxh302EgIycwkWxe7XntmLW-j_WusbJOYRCs3Jms3CU,9892
|
|
11
|
+
abstractassistant/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
|
|
12
|
+
abstractassistant/ui/chat_bubble.py,sha256=TE6zPtQ46I9grKGAb744wHqk4yO6-und3iif8_33XGk,11357
|
|
13
|
+
abstractassistant/ui/history_dialog.py,sha256=25EVyf3-8Kaw1bZPTZe8G-uqw_KnP2t--OgAjsxC06w,18548
|
|
14
|
+
abstractassistant/ui/provider_manager.py,sha256=9IM-BxIs6lUlk6cDCBi7oZFMXmn4CFMlxh0s-_vhzXY,8403
|
|
15
|
+
abstractassistant/ui/qt_bubble.py,sha256=GO0IvM_04h9obRV6WbFKpJsoAwuftYkkljLtmw5XCYU,96483
|
|
16
|
+
abstractassistant/ui/toast_manager.py,sha256=1aU4DPo-J45bC61gTEctHq98ZrHIFxRfZa_9Q8KF588,13721
|
|
17
|
+
abstractassistant/ui/toast_window.py,sha256=BRSwEBlaND5LLipn1HOX0ISWxVH-zOHsYplFkiPaj_g,21727
|
|
18
|
+
abstractassistant/ui/tts_state_manager.py,sha256=UF_zrfl9wf0hNHBGxevcoKxW5Dh7zXibUSVoSSjGP4o,10565
|
|
19
|
+
abstractassistant/ui/ui_styles.py,sha256=FvE2CVUbHmHu1PKVTBBGyhbt781qh4WjLMrHviln39s,13120
|
|
20
|
+
abstractassistant/utils/__init__.py,sha256=7Q3BxyXETkt3tm5trhuLTyL8PoECOK0QiK-0KUVAR2Q,16
|
|
21
|
+
abstractassistant/utils/icon_generator.py,sha256=BLL0ULngPA3QGz_jJga491Fo3tnNi0fx5CcIzvLoVxg,16203
|
|
22
|
+
abstractassistant/utils/markdown_renderer.py,sha256=u5tVIhulSwRYADiqJcZNoHhU8e6pJVgzrwZRd61Bov0,12585
|
|
23
|
+
abstractassistant-0.3.0.dist-info/licenses/LICENSE,sha256=QUjFNAE-0yOkW9-Rle2axkpkt9H7xiZ2VbN-VeONhxc,1106
|
|
24
|
+
abstractassistant-0.3.0.dist-info/METADATA,sha256=noWV-vlbNqvER5tPC8K2Ky1gl8V0qyGYdqpCkwX54sI,11320
|
|
25
|
+
abstractassistant-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
abstractassistant-0.3.0.dist-info/entry_points.txt,sha256=MIzeCh0XG6MbhIzBHtkdEjmjxYBsQrGFevq8Y1L8Jkc,118
|
|
27
|
+
abstractassistant-0.3.0.dist-info/top_level.txt,sha256=oEcSXZAqbflTfZRfF4dogUq6TC1Nqyplq4JgC0CZnLI,34
|
|
28
|
+
abstractassistant-0.3.0.dist-info/RECORD,,
|