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.
@@ -70,30 +70,109 @@ class LLMManager:
70
70
  self._initialize_llm()
71
71
 
72
72
  def _initialize_llm(self):
73
- """Initialize the LLM with current provider and model."""
73
+ """Initialize the LLM with current provider and model.
74
+
75
+ CRITICAL: This method ONLY initializes the LLM provider connection.
76
+ It does NOT create a new session to preserve chat history.
77
+ Sessions are only created when explicitly requested.
78
+ """
74
79
  try:
75
80
  if self.debug:
76
- print(f"🔄 Creating LLM with provider={self.current_provider}, model={self.current_model}")
81
+ if self.debug:
82
+ print(f"🔄 Creating LLM with provider={self.current_provider}, model={self.current_model}")
83
+
84
+ old_llm = self.llm # Keep reference to old LLM
77
85
  self.llm = create_llm(
78
86
  self.current_provider,
79
87
  model=self.current_model,
80
88
  execute_tools=True # Enable automatic tool execution
81
89
  )
82
90
  if self.debug:
83
- print(f"✅ LLM created successfully")
91
+ if self.debug:
92
+ print(f"✅ LLM created successfully")
84
93
 
85
- # Create new session with the LLM and tools
86
- self.create_new_session()
94
+ # CRITICAL FIX: Only create a session if we don't have one yet
95
+ # This preserves existing sessions when switching providers/models
96
+ if self.current_session is None:
97
+ if self.debug:
98
+ if self.debug:
99
+ print("🆕 No existing session - creating initial session")
100
+ self.create_new_session()
101
+ else:
102
+ # Update existing session with new LLM while preserving history
103
+ if self.debug:
104
+ if self.debug:
105
+ print("🔄 Updating existing session with new LLM (preserving history)")
106
+ self._update_session_llm()
87
107
 
88
108
  # Use AbstractCore's built-in token detection
89
109
  self._update_token_limits_from_abstractcore()
90
110
 
91
111
  except Exception as e:
92
- print(f"❌ Error initializing LLM: {e}")
112
+ if self.debug:
113
+ print(f"❌ Error initializing LLM: {e}")
93
114
  import traceback
94
115
  traceback.print_exc()
95
116
  # Keep previous LLM if initialization fails
96
117
 
118
+ def _update_session_llm(self):
119
+ """Update existing session with new LLM while preserving message history.
120
+
121
+ This method allows switching providers/models without losing chat history.
122
+ """
123
+ if not self.current_session or not self.llm:
124
+ return
125
+
126
+ try:
127
+ # Get current session messages to preserve history
128
+ existing_messages = getattr(self.current_session, 'messages', [])
129
+ existing_system_prompt = getattr(self.current_session, 'system_prompt', None)
130
+
131
+ # Prepare tools list (same as in create_new_session)
132
+ tools = []
133
+ if TOOLS_AVAILABLE:
134
+ tools = [
135
+ list_files, search_files, read_file, edit_file,
136
+ write_file, execute_command, web_search
137
+ ]
138
+
139
+ # Create new session with new LLM but preserve system prompt
140
+ system_prompt = existing_system_prompt or (
141
+ """
142
+ You are a helpful AI assistant who has access to tools to help the user.
143
+ Always be a critical and creative thinker who leverage constructive skepticism to progress and evolve its reasoning and answers.
144
+ Always answer in nicely formatted markdown.
145
+ """
146
+ )
147
+
148
+ # Create new session with preserved system prompt
149
+ new_session = BasicSession(
150
+ self.llm,
151
+ system_prompt=system_prompt,
152
+ tools=tools
153
+ )
154
+
155
+ # Restore message history by replaying messages
156
+ # Skip system message (first message) as it's already set
157
+ for msg in existing_messages[1:] if len(existing_messages) > 1 else []:
158
+ if hasattr(msg, 'role') and hasattr(msg, 'content'):
159
+ # Add message to new session's history without generating response
160
+ new_session.messages.append(msg)
161
+
162
+ # Replace current session
163
+ self.current_session = new_session
164
+
165
+ if self.debug:
166
+ if self.debug:
167
+ print(f"🔄 Session updated with new LLM - preserved {len(existing_messages)} messages")
168
+
169
+ except Exception as e:
170
+ if self.debug:
171
+ if self.debug:
172
+ print(f"❌ Error updating session LLM (preserving existing session): {e}")
173
+ # CRITICAL: Do NOT create new session on error - preserve existing session
174
+ # The user's chat history is more important than a perfect LLM update
175
+
97
176
  def _update_token_limits_from_abstractcore(self):
98
177
  """Update token limits using AbstractCore's built-in detection."""
99
178
  if self.llm:
@@ -104,16 +183,21 @@ class LLMManager:
104
183
 
105
184
  if self.debug:
106
185
  # Show AbstractCore's token configuration
107
- print(f"📊 {self.llm.get_token_configuration_summary()}")
186
+ if self.debug:
187
+ print(f"📊 {self.llm.get_token_configuration_summary()}")
108
188
 
109
189
  def create_new_session(self, tts_mode: bool = False):
110
190
  """Create a new session with tools - CLEAN AND SIMPLE as per AbstractCore docs.
111
191
 
192
+ WARNING: This method creates a completely new session, destroying existing chat history.
193
+ Use update_session_mode() to switch TTS mode while preserving history.
194
+
112
195
  Args:
113
196
  tts_mode: If True, use concise prompts optimized for text-to-speech
114
197
  """
115
198
  if not self.llm:
116
- print("❌ No LLM available - cannot create session")
199
+ if self.debug:
200
+ print("❌ No LLM available - cannot create session")
117
201
  return
118
202
 
119
203
  # Prepare tools list
@@ -124,7 +208,8 @@ class LLMManager:
124
208
  write_file, execute_command, web_search
125
209
  ]
126
210
  if self.debug:
127
- print(f"🔧 Registering {len(tools)} tools with session")
211
+ if self.debug:
212
+ print(f"🔧 Registering {len(tools)} tools with session")
128
213
 
129
214
  # Choose system prompt based on TTS mode
130
215
  if tts_mode:
@@ -155,9 +240,84 @@ class LLMManager:
155
240
 
156
241
  if self.debug:
157
242
  if TOOLS_AVAILABLE:
158
- print(f"✅ Created new AbstractCore session with tools ({'TTS mode' if tts_mode else 'normal mode'})")
243
+ if self.debug:
244
+ print(f"✅ Created new AbstractCore session with tools ({'TTS mode' if tts_mode else 'normal mode'})")
245
+ else:
246
+ if self.debug:
247
+ print(f"✅ Created new AbstractCore session (no tools available, {'TTS mode' if tts_mode else 'normal mode'})")
248
+
249
+ def update_session_mode(self, tts_mode: bool = False):
250
+ """Update session mode (TTS vs normal) while preserving chat history.
251
+
252
+ This method changes the system prompt behavior without destroying the session.
253
+
254
+ Args:
255
+ tts_mode: If True, switch to TTS-optimized mode; if False, switch to normal mode
256
+ """
257
+ if not self.current_session:
258
+ # No existing session - only create if this is initial startup
259
+ if self.debug:
260
+ if self.debug:
261
+ print("⚠️ No session exists - creating initial session for mode update")
262
+ self.create_new_session(tts_mode=tts_mode)
263
+ return
264
+
265
+ try:
266
+ # Get current session messages to preserve history
267
+ existing_messages = getattr(self.current_session, 'messages', [])
268
+
269
+ # Prepare tools list
270
+ tools = []
271
+ if TOOLS_AVAILABLE:
272
+ tools = [
273
+ list_files, search_files, read_file, edit_file,
274
+ write_file, execute_command, web_search
275
+ ]
276
+
277
+ # Choose system prompt based on TTS mode
278
+ if tts_mode:
279
+ system_prompt = (
280
+ """
281
+ You are a Helpful Voice Assistant. By design, your answers are short and more conversational, unless specifically asked to detail something.
282
+ You only speak, so never use any text formatting or markdown. Write for a speaker.
283
+ """
284
+ )
159
285
  else:
160
- print(f"✅ Created new AbstractCore session (no tools available, {'TTS mode' if tts_mode else 'normal mode'})")
286
+ system_prompt = (
287
+ """
288
+ You are a helpful AI assistant who has access to tools to help the user.
289
+ Always be a critical and creative thinker who leverage constructive skepticism to progress and evolve its reasoning and answers.
290
+ Always answer in nicely formatted markdown.
291
+ """
292
+ )
293
+
294
+ # Create new session with updated system prompt
295
+ new_session = BasicSession(
296
+ self.llm,
297
+ system_prompt=system_prompt,
298
+ tools=tools
299
+ )
300
+
301
+ # Restore message history by replaying messages
302
+ # Skip system message (first message) as it's already set with new prompt
303
+ for msg in existing_messages[1:] if len(existing_messages) > 1 else []:
304
+ if hasattr(msg, 'role') and hasattr(msg, 'content'):
305
+ # Add message to new session's history without generating response
306
+ new_session.messages.append(msg)
307
+
308
+ # Replace current session
309
+ self.current_session = new_session
310
+
311
+ if self.debug:
312
+ if self.debug:
313
+ print(f"🔄 Session mode updated to {'TTS' if tts_mode else 'normal'} - preserved {len(existing_messages)} messages")
314
+
315
+ except Exception as e:
316
+ if self.debug:
317
+ if self.debug:
318
+ print(f"❌ Error updating session mode (preserving existing session): {e}")
319
+ # CRITICAL: Do NOT create new session on error - preserve existing session
320
+ # The user's chat history is more important than a perfect mode switch
161
321
 
162
322
  def clear_session(self):
163
323
  """Clear current session and create a new one."""
@@ -168,19 +328,22 @@ class LLMManager:
168
328
  try:
169
329
  if not self.current_session:
170
330
  if self.debug:
171
- print("⚠️ No session to save")
331
+ if self.debug:
332
+ print("⚠️ No session to save")
172
333
  return False
173
334
 
174
335
  # Use AbstractCore's built-in save method
175
336
  self.current_session.save(filepath)
176
337
 
177
338
  if self.debug:
178
- print(f"✅ Session saved to {filepath}")
339
+ if self.debug:
340
+ print(f"✅ Session saved to {filepath}")
179
341
  return True
180
342
 
181
343
  except Exception as e:
182
344
  if self.debug:
183
- print(f"❌ Error saving session: {e}")
345
+ if self.debug:
346
+ print(f"❌ Error saving session: {e}")
184
347
  return False
185
348
 
186
349
  def load_session(self, filepath: str):
@@ -201,12 +364,14 @@ class LLMManager:
201
364
  self._update_token_limits_from_abstractcore()
202
365
 
203
366
  if self.debug:
204
- print(f"✅ Session loaded from {filepath}")
367
+ if self.debug:
368
+ print(f"✅ Session loaded from {filepath}")
205
369
  return True
206
370
 
207
371
  except Exception as e:
208
372
  if self.debug:
209
- print(f"❌ Error loading session: {e}")
373
+ if self.debug:
374
+ print(f"❌ Error loading session: {e}")
210
375
  return False
211
376
 
212
377
  def get_providers(self) -> List[Dict[str, Any]]:
@@ -219,7 +384,8 @@ class LLMManager:
219
384
  return get_available_models_for_provider(provider)
220
385
  except Exception as e:
221
386
  if self.debug:
222
- print(f"⚠️ Could not get models for {provider}: {e}")
387
+ if self.debug:
388
+ print(f"⚠️ Could not get models for {provider}: {e}")
223
389
  return []
224
390
 
225
391
  def set_provider(self, provider: str, model: Optional[str] = None):
@@ -235,7 +401,8 @@ class LLMManager:
235
401
  # Reinitialize LLM
236
402
  self._initialize_llm()
237
403
  elif self.debug:
238
- print(f"⚠️ Provider {provider} not available")
404
+ if self.debug:
405
+ print(f"⚠️ Provider {provider} not available")
239
406
 
240
407
  def set_model(self, model: str):
241
408
  """Set the active model for current provider."""
@@ -261,8 +428,11 @@ class LLMManager:
261
428
  self.set_model(model)
262
429
 
263
430
  try:
264
- # Ensure we have a session
431
+ # Ensure we have a session - but only create if absolutely necessary
265
432
  if self.current_session is None:
433
+ if self.debug:
434
+ if self.debug:
435
+ print("⚠️ No session exists - creating initial session for first use")
266
436
  self.create_new_session()
267
437
 
268
438
  # Generate response using session with optional media files
@@ -23,16 +23,43 @@ class VoiceManager:
23
23
  """
24
24
  self.debug_mode = debug_mode
25
25
  self._abstractvoice_manager = None
26
+
27
+ # Callbacks for speech start/end events
28
+ self.on_speech_start = None
29
+ self.on_speech_end = None
26
30
 
27
31
  try:
28
32
  self._abstractvoice_manager = AbstractVoiceManager(debug_mode=debug_mode)
33
+
34
+ # Set up NEW v0.5.1 precise audio callbacks (not synthesis callbacks)
35
+ self._abstractvoice_manager.on_audio_start = self._on_audio_start
36
+ self._abstractvoice_manager.on_audio_end = self._on_audio_end
37
+
29
38
  if self.debug_mode:
30
- print("🔊 AbstractVoice initialized successfully")
39
+ if self.debug_mode:
40
+ print("🔊 AbstractVoice v0.5.1 initialized with precise audio callbacks")
31
41
  except Exception as e:
32
42
  if self.debug_mode:
33
- print(f"❌ AbstractVoice initialization failed: {e}")
43
+ if self.debug_mode:
44
+ print(f"❌ AbstractVoice initialization failed: {e}")
34
45
  raise RuntimeError(f"Failed to initialize AbstractVoice: {e}")
35
46
 
47
+ def _on_audio_start(self):
48
+ """Called when audio stream actually starts playing (v0.5.1 precise timing)."""
49
+ if self.debug_mode:
50
+ if self.debug_mode:
51
+ print("🔊 Audio stream started - user can hear speech")
52
+ if self.on_speech_start:
53
+ self.on_speech_start()
54
+
55
+ def _on_audio_end(self):
56
+ """Called when audio stream actually ends (v0.5.1 precise timing)."""
57
+ if self.debug_mode:
58
+ if self.debug_mode:
59
+ print("🔊 Audio stream ended - ready for next action")
60
+ if self.on_speech_end:
61
+ self.on_speech_end()
62
+
36
63
  def is_available(self) -> bool:
37
64
  """Check if TTS is available."""
38
65
  return True # AbstractVoice is a required dependency, always available after construction
@@ -54,7 +81,8 @@ class VoiceManager:
54
81
  """
55
82
  if not text.strip():
56
83
  if self.debug_mode:
57
- print("❌ Empty text provided to TTS")
84
+ if self.debug_mode:
85
+ print("❌ Empty text provided to TTS")
58
86
  return False
59
87
 
60
88
  try:
@@ -62,7 +90,8 @@ class VoiceManager:
62
90
  return True
63
91
  except Exception as e:
64
92
  if self.debug_mode:
65
- print(f"❌ AbstractVoice speak error: {e}")
93
+ if self.debug_mode:
94
+ print(f"❌ AbstractVoice speak error: {e}")
66
95
  return False
67
96
 
68
97
  def pause(self) -> bool:
@@ -74,11 +103,13 @@ class VoiceManager:
74
103
  try:
75
104
  success = self._abstractvoice_manager.pause_speaking()
76
105
  if self.debug_mode:
77
- print(f"🔊 AbstractVoice speech {'paused' if success else 'pause failed'}")
106
+ if self.debug_mode:
107
+ print(f"🔊 AbstractVoice speech {'paused' if success else 'pause failed'}")
78
108
  return success
79
109
  except Exception as e:
80
110
  if self.debug_mode:
81
- print(f"❌ Error pausing AbstractVoice: {e}")
111
+ if self.debug_mode:
112
+ print(f"❌ Error pausing AbstractVoice: {e}")
82
113
  return False
83
114
 
84
115
  def resume(self) -> bool:
@@ -90,11 +121,13 @@ class VoiceManager:
90
121
  try:
91
122
  success = self._abstractvoice_manager.resume_speaking()
92
123
  if self.debug_mode:
93
- print(f"🔊 AbstractVoice speech {'resumed' if success else 'resume failed'}")
124
+ if self.debug_mode:
125
+ print(f"🔊 AbstractVoice speech {'resumed' if success else 'resume failed'}")
94
126
  return success
95
127
  except Exception as e:
96
128
  if self.debug_mode:
97
- print(f"❌ Error resuming AbstractVoice: {e}")
129
+ if self.debug_mode:
130
+ print(f"❌ Error resuming AbstractVoice: {e}")
98
131
  return False
99
132
 
100
133
  def is_paused(self) -> bool:
@@ -103,7 +136,8 @@ class VoiceManager:
103
136
  return self._abstractvoice_manager.is_paused()
104
137
  except Exception as e:
105
138
  if self.debug_mode:
106
- print(f"❌ Error checking pause state: {e}")
139
+ if self.debug_mode:
140
+ print(f"❌ Error checking pause state: {e}")
107
141
  return False
108
142
 
109
143
  def get_state(self) -> str:
@@ -121,7 +155,8 @@ class VoiceManager:
121
155
  return 'idle'
122
156
  except Exception as e:
123
157
  if self.debug_mode:
124
- print(f"❌ Error getting TTS state: {e}")
158
+ if self.debug_mode:
159
+ print(f"❌ Error getting TTS state: {e}")
125
160
  return 'idle'
126
161
 
127
162
  def stop(self):
@@ -129,20 +164,24 @@ class VoiceManager:
129
164
  try:
130
165
  self._abstractvoice_manager.stop_speaking()
131
166
  if self.debug_mode:
132
- print("🔊 AbstractVoice speech stopped")
167
+ if self.debug_mode:
168
+ print("🔊 AbstractVoice speech stopped")
133
169
  except Exception as e:
134
170
  if self.debug_mode:
135
- print(f"❌ Error stopping AbstractVoice: {e}")
171
+ if self.debug_mode:
172
+ print(f"❌ Error stopping AbstractVoice: {e}")
136
173
 
137
174
  def cleanup(self):
138
175
  """Clean up TTS resources."""
139
176
  try:
140
177
  self._abstractvoice_manager.cleanup()
141
178
  if self.debug_mode:
142
- print("🔊 AbstractVoice cleaned up")
179
+ if self.debug_mode:
180
+ print("🔊 AbstractVoice cleaned up")
143
181
  except Exception as e:
144
182
  if self.debug_mode:
145
- print(f"❌ Error cleaning up AbstractVoice: {e}")
183
+ if self.debug_mode:
184
+ print(f"❌ Error cleaning up AbstractVoice: {e}")
146
185
 
147
186
  # STT (Speech-to-Text) Methods for Full Voice Mode
148
187
 
@@ -156,13 +195,16 @@ class VoiceManager:
156
195
  try:
157
196
  self._abstractvoice_manager.set_voice_mode(mode)
158
197
  if self.debug_mode:
159
- print(f"🔊 Voice mode set to: {mode}")
198
+ if self.debug_mode:
199
+ print(f"🔊 Voice mode set to: {mode}")
160
200
  except Exception as e:
161
201
  if self.debug_mode:
162
- print(f"❌ Error setting voice mode: {e}")
202
+ if self.debug_mode:
203
+ print(f"❌ Error setting voice mode: {e}")
163
204
  else:
164
205
  if self.debug_mode:
165
- print(f"⚠️ Voice mode setting not available, simulating mode: {mode}")
206
+ if self.debug_mode:
207
+ print(f"⚠️ Voice mode setting not available, simulating mode: {mode}")
166
208
 
167
209
  def listen(self, on_transcription: Callable[[str], None], on_stop: Callable[[], None] = None):
168
210
  """Start listening for speech input.
@@ -178,14 +220,17 @@ class VoiceManager:
178
220
  on_stop=on_stop
179
221
  )
180
222
  if self.debug_mode:
181
- print("🎤 Started listening for speech")
223
+ if self.debug_mode:
224
+ print("🎤 Started listening for speech")
182
225
  except Exception as e:
183
226
  if self.debug_mode:
184
- print(f"❌ Error starting listening: {e}")
227
+ if self.debug_mode:
228
+ print(f"❌ Error starting listening: {e}")
185
229
  raise
186
230
  else:
187
231
  if self.debug_mode:
188
- print("⚠️ STT listening not available in current AbstractVoice version")
232
+ if self.debug_mode:
233
+ print("⚠️ STT listening not available in current AbstractVoice version")
189
234
  raise RuntimeError("STT listening not available")
190
235
 
191
236
  def stop_listening(self):
@@ -194,13 +239,16 @@ class VoiceManager:
194
239
  try:
195
240
  self._abstractvoice_manager.stop_listening()
196
241
  if self.debug_mode:
197
- print("🎤 Stopped listening for speech")
242
+ if self.debug_mode:
243
+ print("🎤 Stopped listening for speech")
198
244
  except Exception as e:
199
245
  if self.debug_mode:
200
- print(f"❌ Error stopping listening: {e}")
246
+ if self.debug_mode:
247
+ print(f"❌ Error stopping listening: {e}")
201
248
  else:
202
249
  if self.debug_mode:
203
- print("⚠️ Stop listening not available in current AbstractVoice version")
250
+ if self.debug_mode:
251
+ print("⚠️ Stop listening not available in current AbstractVoice version")
204
252
 
205
253
  def is_listening(self) -> bool:
206
254
  """Check if currently listening for speech."""
@@ -209,11 +257,13 @@ class VoiceManager:
209
257
  return self._abstractvoice_manager.is_listening()
210
258
  except Exception as e:
211
259
  if self.debug_mode:
212
- print(f"❌ Error checking listening state: {e}")
260
+ if self.debug_mode:
261
+ print(f"❌ Error checking listening state: {e}")
213
262
  return False
214
263
  else:
215
264
  if self.debug_mode:
216
- print("⚠️ Listening state check not available")
265
+ if self.debug_mode:
266
+ print("⚠️ Listening state check not available")
217
267
  return False
218
268
 
219
269
 
@@ -14,7 +14,12 @@ def main():
14
14
  """Create macOS app bundle for AbstractAssistant."""
15
15
  try:
16
16
  # Import the app bundle generator
17
- from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
17
+ try:
18
+ import setup_macos_app
19
+ MacOSAppBundleGenerator = setup_macos_app.MacOSAppBundleGenerator
20
+ except ImportError:
21
+ # Fallback: try importing from abstractassistant package
22
+ from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
18
23
 
19
24
  # Find the package directory
20
25
  import abstractassistant
@@ -240,7 +240,7 @@ class iPhoneMessagesDialog:
240
240
  background: transparent;
241
241
  border: none;
242
242
  text-align: left;
243
- font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
243
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
244
244
  }
245
245
  """)
246
246
  nav_layout.addWidget(back_btn)
@@ -254,7 +254,7 @@ class iPhoneMessagesDialog:
254
254
  color: #ffffff;
255
255
  font-size: 17px;
256
256
  font-weight: 600;
257
- font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
257
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
258
258
  }
259
259
  """)
260
260
  nav_layout.addWidget(title)
@@ -328,7 +328,7 @@ class iPhoneMessagesDialog:
328
328
  font-size: 14px;
329
329
  font-weight: 400;
330
330
  line-height: 18px;
331
- font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
331
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
332
332
  }
333
333
  """)
334
334
  # Right align
@@ -351,7 +351,7 @@ class iPhoneMessagesDialog:
351
351
  font-size: 14px;
352
352
  font-weight: 400;
353
353
  line-height: 18px;
354
- font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
354
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
355
355
  }
356
356
  """)
357
357
  # Left align
@@ -394,7 +394,7 @@ class iPhoneMessagesDialog:
394
394
  font-size: 13px;
395
395
  font-weight: 400;
396
396
  color: rgba(255, 255, 255, 0.6);
397
- font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
397
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
398
398
  padding: 0px;
399
399
  }
400
400
  """)