pipulate 1.76__tar.gz → 1.78__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 (87) hide show
  1. {pipulate-1.76/pipulate.egg-info → pipulate-1.78}/PKG-INFO +1 -1
  2. {pipulate-1.76 → pipulate-1.78}/__init__.py +2 -2
  3. {pipulate-1.76 → pipulate-1.78}/apps/010_introduction.py +50 -50
  4. {pipulate-1.76 → pipulate-1.78}/imports/voice_synthesis.py +5 -0
  5. {pipulate-1.76 → pipulate-1.78}/pipulate/core.py +32 -0
  6. {pipulate-1.76 → pipulate-1.78/pipulate.egg-info}/PKG-INFO +1 -1
  7. {pipulate-1.76 → pipulate-1.78}/pyproject.toml +1 -1
  8. {pipulate-1.76 → pipulate-1.78}/server.py +40 -39
  9. {pipulate-1.76 → pipulate-1.78}/LICENSE +0 -0
  10. {pipulate-1.76 → pipulate-1.78}/README.md +0 -0
  11. {pipulate-1.76 → pipulate-1.78}/apps/015_config.py +0 -0
  12. {pipulate-1.76 → pipulate-1.78}/apps/020_profiles.py +0 -0
  13. {pipulate-1.76 → pipulate-1.78}/apps/025_aspect.py +0 -0
  14. {pipulate-1.76 → pipulate-1.78}/apps/030_roles.py +0 -0
  15. {pipulate-1.76 → pipulate-1.78}/apps/040_hello_workflow.py +0 -0
  16. {pipulate-1.76 → pipulate-1.78}/apps/050_documentation.py +0 -0
  17. {pipulate-1.76 → pipulate-1.78}/apps/060_tasks.py +0 -0
  18. {pipulate-1.76 → pipulate-1.78}/apps/070_history.py +0 -0
  19. {pipulate-1.76 → pipulate-1.78}/apps/100_connect_with_botify.py +0 -0
  20. {pipulate-1.76 → pipulate-1.78}/apps/110_parameter_buster.py +0 -0
  21. {pipulate-1.76 → pipulate-1.78}/apps/120_link_graph.py +0 -0
  22. {pipulate-1.76 → pipulate-1.78}/apps/130_gap_analysis.py +0 -0
  23. {pipulate-1.76 → pipulate-1.78}/apps/200_workflow_genesis.py +0 -0
  24. {pipulate-1.76 → pipulate-1.78}/apps/210_widget_examples.py +0 -0
  25. {pipulate-1.76 → pipulate-1.78}/apps/220_roadmap.py +0 -0
  26. {pipulate-1.76 → pipulate-1.78}/apps/230_dev_assistant.py +0 -0
  27. {pipulate-1.76 → pipulate-1.78}/apps/240_simon_mcp.py +0 -0
  28. {pipulate-1.76 → pipulate-1.78}/apps/300_blank_placeholder.py +0 -0
  29. {pipulate-1.76 → pipulate-1.78}/apps/400_botify_trifecta.py +0 -0
  30. {pipulate-1.76 → pipulate-1.78}/apps/440_browser_automation.py +0 -0
  31. {pipulate-1.76 → pipulate-1.78}/apps/450_stream_simulator.py +0 -0
  32. {pipulate-1.76 → pipulate-1.78}/apps/510_text_field.py +0 -0
  33. {pipulate-1.76 → pipulate-1.78}/apps/520_text_area.py +0 -0
  34. {pipulate-1.76 → pipulate-1.78}/apps/530_dropdown.py +0 -0
  35. {pipulate-1.76 → pipulate-1.78}/apps/540_checkboxes.py +0 -0
  36. {pipulate-1.76 → pipulate-1.78}/apps/550_radios.py +0 -0
  37. {pipulate-1.76 → pipulate-1.78}/apps/560_range.py +0 -0
  38. {pipulate-1.76 → pipulate-1.78}/apps/570_switch.py +0 -0
  39. {pipulate-1.76 → pipulate-1.78}/apps/580_upload.py +0 -0
  40. {pipulate-1.76 → pipulate-1.78}/apps/610_markdown.py +0 -0
  41. {pipulate-1.76 → pipulate-1.78}/apps/620_mermaid.py +0 -0
  42. {pipulate-1.76 → pipulate-1.78}/apps/630_prism.py +0 -0
  43. {pipulate-1.76 → pipulate-1.78}/apps/640_javascript.py +0 -0
  44. {pipulate-1.76 → pipulate-1.78}/apps/710_pandas.py +0 -0
  45. {pipulate-1.76 → pipulate-1.78}/apps/720_rich.py +0 -0
  46. {pipulate-1.76 → pipulate-1.78}/apps/730_matplotlib.py +0 -0
  47. {pipulate-1.76 → pipulate-1.78}/apps/810_webbrowser.py +0 -0
  48. {pipulate-1.76 → pipulate-1.78}/apps/820_selenium.py +0 -0
  49. {pipulate-1.76 → pipulate-1.78}/apps/xx_link_graph_v2.py +0 -0
  50. {pipulate-1.76 → pipulate-1.78}/cli.py +0 -0
  51. {pipulate-1.76 → pipulate-1.78}/config.py +0 -0
  52. {pipulate-1.76 → pipulate-1.78}/imports/__init__.py +0 -0
  53. {pipulate-1.76 → pipulate-1.78}/imports/ai_dictdb.py +0 -0
  54. {pipulate-1.76 → pipulate-1.78}/imports/ai_tool_discovery_simple_parser.py +0 -0
  55. {pipulate-1.76 → pipulate-1.78}/imports/append_only_conversation.py +0 -0
  56. {pipulate-1.76 → pipulate-1.78}/imports/ascii_displays.py +0 -0
  57. {pipulate-1.76 → pipulate-1.78}/imports/botify/__init__.py +0 -0
  58. {pipulate-1.76 → pipulate-1.78}/imports/botify/code_generators.py +0 -0
  59. {pipulate-1.76 → pipulate-1.78}/imports/botify/true_schema_discoverer.py +0 -0
  60. {pipulate-1.76 → pipulate-1.78}/imports/botify_code_generation.py +0 -0
  61. {pipulate-1.76 → pipulate-1.78}/imports/crud.py +0 -0
  62. {pipulate-1.76 → pipulate-1.78}/imports/database_safety_wrapper.py +0 -0
  63. {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/__init__.py +0 -0
  64. {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/ai_dom_beautifier.py +0 -0
  65. {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/enhanced_dom_processor.py +0 -0
  66. {pipulate-1.76 → pipulate-1.78}/imports/durable_backup_system.py +0 -0
  67. {pipulate-1.76 → pipulate-1.78}/imports/mcp_orchestrator.py +0 -0
  68. {pipulate-1.76 → pipulate-1.78}/imports/server_logging.py +0 -0
  69. {pipulate-1.76 → pipulate-1.78}/imports/stream_orchestrator.py +0 -0
  70. {pipulate-1.76 → pipulate-1.78}/pipulate/__init__.py +0 -0
  71. {pipulate-1.76 → pipulate-1.78}/pipulate/pipulate.py +0 -0
  72. {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/SOURCES.txt +0 -0
  73. {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/dependency_links.txt +0 -0
  74. {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/entry_points.txt +0 -0
  75. {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/requires.txt +0 -0
  76. {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/top_level.txt +0 -0
  77. {pipulate-1.76 → pipulate-1.78}/setup.cfg +0 -0
  78. {pipulate-1.76 → pipulate-1.78}/tools/__init__.py +0 -0
  79. {pipulate-1.76 → pipulate-1.78}/tools/advanced_automation_tools.py +0 -0
  80. {pipulate-1.76 → pipulate-1.78}/tools/botify_tools.py +0 -0
  81. {pipulate-1.76 → pipulate-1.78}/tools/conversation_tools.py +0 -0
  82. {pipulate-1.76 → pipulate-1.78}/tools/dom_tools.py +0 -0
  83. {pipulate-1.76 → pipulate-1.78}/tools/keychain_tools.py +0 -0
  84. {pipulate-1.76 → pipulate-1.78}/tools/llm_optics.py +0 -0
  85. {pipulate-1.76 → pipulate-1.78}/tools/mcp_tools.py +0 -0
  86. {pipulate-1.76 → pipulate-1.78}/tools/scraper_tools.py +0 -0
  87. {pipulate-1.76 → pipulate-1.78}/tools/system_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 1.76
3
+ Version: 1.78
4
4
  Summary: A Notebook-inspired Hand-cranked Agentic Framework
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: MIT
@@ -12,8 +12,8 @@ Usage:
12
12
  pipulate
13
13
  """
14
14
 
15
- __version__ = "1.76"
16
- __version_description__ = "Unified Platform Detection"
15
+ __version__ = "1.78"
16
+ __version_description__ = "Global Voice Toggle"
17
17
  __email__ = "pipulate@gmail.com"
18
18
  __description__ = "A Notebook-inspired Hand-cranked Agentic Framework"
19
19
 
@@ -13,6 +13,7 @@ Features:
13
13
  """
14
14
 
15
15
  import time
16
+ import os
16
17
  import asyncio
17
18
  from fasthtml.common import *
18
19
  from loguru import logger
@@ -53,7 +54,6 @@ class IntroductionPlugin:
53
54
 
54
55
  # Register routes
55
56
  pipulate.register_workflow_routes(self)
56
- self.app.route(f'/{self.app_name}/toggle_voice', methods=['POST'])(self.toggle_voice)
57
57
  self.app.route(f'/{self.app_name}/speak/{{step_id}}', methods=['POST'])(self.speak_step)
58
58
 
59
59
  def _get_slide_data(self, step_id: str):
@@ -85,12 +85,16 @@ class IntroductionPlugin:
85
85
 
86
86
  elif not has_configured:
87
87
  # STATE 2: The Guide Persona (Airlock fired, but Configuration is pending)
88
- msg = f"Welcome to {dynamic_app_name}, {operator_name}. I am Chip O'Theseus. To see a demonstration of my capabilities, press Ctrl+Alt+D right now. Otherwise, we will proceed to finalize your configuration."
88
+ msg = (
89
+ f"Welcome to {dynamic_app_name}, {operator_name}. I am Chip O'Theseus. To see a demonstration of my capabilities, press ",
90
+ Strong("Ctrl+Alt+D", cls="platform-shortcut"),
91
+ " right now. Otherwise, we will proceed to finalize your configuration."
92
+ )
89
93
  return "Welcome", msg, 'finalize'
90
94
 
91
95
  else:
92
96
  # STATE 3: The Veteran Persona (Config workflow is finalized)
93
- msg = f"Welcome back to {dynamic_app_name}, {operator_name}. All systems are online and ready. Your primary cognitive engine is locked to {active_model}."
97
+ msg = f"Welcome back to {dynamic_app_name}, {operator_name}. All systems are online and ready."
94
98
  return "Dashboard Ready ✅", msg, None
95
99
 
96
100
  elif step_id == 'finalize':
@@ -99,17 +103,22 @@ class IntroductionPlugin:
99
103
  return "Unknown", "I have nothing to say about this.", None
100
104
 
101
105
 
102
- async def toggle_voice(self, request):
103
- """Toggles the global voice_enabled state."""
104
- current_state = self.wand.db.get('voice_enabled', '0') == '1'
105
- new_state = not current_state
106
- self.wand.db['voice_enabled'] = '1' if new_state else '0'
107
- logger.info(f"🔊 Voice toggled: {new_state}")
108
- return self._render_voice_controls(new_state)
109
-
110
106
  async def speak_step(self, step_id: str):
111
107
  """Trigger server-side audio playback using JIT evaluated text."""
112
- _, text, _ = self._get_slide_data(step_id)
108
+ if step_id == 'step_01':
109
+ sentinel = self.wand.paths.data / '.has_greeted'
110
+ sentinel.touch(exist_ok=True)
111
+
112
+ _, content, _ = self._get_slide_data(step_id)
113
+
114
+ # Convert FastHTML components to string for the voice engine
115
+ if isinstance(content, tuple):
116
+ text = "".join(to_xml(c) if hasattr(c, '__html__') else str(c) for c in content)
117
+ elif hasattr(content, '__html__'):
118
+ text = to_xml(content)
119
+ else:
120
+ text = str(content)
121
+
113
122
 
114
123
  from imports.voice_synthesis import chip_voice_system
115
124
  if chip_voice_system and chip_voice_system.voice_ready:
@@ -119,22 +128,6 @@ class IntroductionPlugin:
119
128
 
120
129
  return ""
121
130
 
122
- def _render_voice_controls(self, is_enabled):
123
- """Renders the Voice Toggle button."""
124
- icon = "🔊" if is_enabled else "🔇"
125
- style = "color: var(--pico-color-green-500); border-color: var(--pico-color-green-500);" if is_enabled else "color: var(--pico-muted-color);"
126
- text = "Voice On" if is_enabled else "Voice Off"
127
-
128
- return Button(
129
- f"{icon} {text}",
130
- hx_post=f"/{self.app_name}/toggle_voice",
131
- hx_swap="outerHTML",
132
- cls="secondary outline",
133
- style=f"{style} margin-bottom: 0; font-size: 0.8rem; padding: 4px 8px;",
134
- id="voice-toggle-btn",
135
- data_testid="voice-toggle"
136
- )
137
-
138
131
  def _render_slide(self, step_id, title, content, next_step_id=None):
139
132
  """Helper to render a standardized slide."""
140
133
  import time
@@ -149,11 +142,18 @@ class IntroductionPlugin:
149
142
  # Auto-speak trigger
150
143
  onload_trigger = ""
151
144
  if voice_enabled:
152
- onload_trigger = Div(
153
- hx_post=f"/{self.app_name}/speak/{step_id}",
154
- hx_trigger=trigger_logic,
155
- style="display:none;"
156
- )
145
+ allow_auto_speak = True
146
+ if step_id == 'step_01':
147
+ sentinel = self.wand.paths.data / '.has_greeted'
148
+ if sentinel.exists():
149
+ allow_auto_speak = False
150
+
151
+ if allow_auto_speak:
152
+ onload_trigger = Div(
153
+ hx_post=f"/{self.app_name}/speak/{step_id}",
154
+ hx_trigger=trigger_logic,
155
+ style="display:none;"
156
+ )
157
157
 
158
158
  # The "Encore" Button (Volume Icon)
159
159
  encore_btn = A(
@@ -178,6 +178,11 @@ class IntroductionPlugin:
178
178
  )
179
179
  )
180
180
 
181
+ if isinstance(content, tuple):
182
+ content_tag = P(*content, style="font-size: 1.3rem; line-height: 1.6; margin-bottom: 2rem;")
183
+ else:
184
+ content_tag = P(content, style="font-size: 1.3rem; line-height: 1.6; margin-bottom: 2rem;")
185
+
181
186
  # JIT Client-Side Text Replacement for Shortcuts
182
187
  shortcut_script = Script('''
183
188
  const shortcutEl = document.querySelector('.platform-shortcut');
@@ -187,25 +192,20 @@ class IntroductionPlugin:
187
192
  ''')
188
193
 
189
194
  return Div(
190
- onload_trigger,
191
- Card(
192
- Div(
195
+ onload_trigger,
196
+ Card(
193
197
  Div(
194
- H2(title, style="display: inline-block; margin-bottom: 0;"),
195
- encore_btn,
196
- style="display: flex; align-items: center;"
197
- ),
198
- self._render_voice_controls(voice_enabled),
199
- style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;"
200
- ),
201
- P(content, style="font-size: 1.3rem; line-height: 1.6; margin-bottom: 2rem;"),
202
- Div(*nav_buttons, style="display: flex; justify-content: flex-end;"),
203
- id=step_id,
204
- cls="intro-slide",
205
- ),
206
- shortcut_script
198
+ H2(title, style="display: inline-block; margin-bottom: 0;"),
199
+ encore_btn,
200
+ style="display: flex; align-items: center; margin-bottom: 2rem;"
201
+ ),
202
+ content_tag,
203
+ Div(*nav_buttons, style="display: flex; justify-content: flex-end;"),
204
+ id=step_id,
205
+ cls="intro-slide",
206
+ ),
207
+ shortcut_script
207
208
  )
208
-
209
209
  # --- Step Handlers ---
210
210
 
211
211
  async def landing(self, request):
@@ -142,6 +142,11 @@ class ChipVoiceSystem:
142
142
  import re
143
143
  spoken_text = re.sub(r'\*?\*?MikeLev\.in\*?\*?:?', 'Mike:', text, flags=re.IGNORECASE)
144
144
  # ---------------------------------------------------------
145
+
146
+ # 🛡️ THE ACOUSTIC SANITIZER: Purge visual markup before synthesis
147
+ spoken_text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', spoken_text) # Extract markdown link text
148
+ spoken_text = re.sub(r'\[[^\]]+\]', '', spoken_text) # Remove silent bracket tags
149
+ spoken_text = re.sub(r'<[^>]+>', '', spoken_text) # Strip HTML tags
145
150
 
146
151
  try:
147
152
  # Use temporary file for audio output
@@ -3,6 +3,7 @@ import importlib
3
3
  from pathlib import Path
4
4
  import functools
5
5
  import json
6
+ import html
6
7
  import os
7
8
  import re
8
9
  from dotenv import load_dotenv, set_key
@@ -3080,3 +3081,34 @@ class Pipulate:
3080
3081
  except Exception as e:
3081
3082
  logger.error(f"❌ Airlock Failure: {e}")
3082
3083
  return False
3084
+
3085
+ def copy_button(self, text_to_copy: str, label: str = "Copy Markdown"):
3086
+ """
3087
+ Injects a web-native Copy to Clipboard button directly into the Jupyter output.
3088
+ Reuses the global SVG icon for aesthetic consistency.
3089
+ """
3090
+ if not self.is_notebook_context:
3091
+ return # Only render in Jupyter
3092
+
3093
+ import json
3094
+ import html
3095
+ from IPython.display import display, HTML
3096
+
3097
+ # Grab the exact same SVG used in the Chat UI
3098
+ clipboard_svg = CFG.SVG_ICONS.get('CLIPBOARD', '📋')
3099
+
3100
+ # Safely escape the text for JS, and then escape the JS for HTML attribute injection
3101
+ safe_js_string = json.dumps(text_to_copy)
3102
+ safe_html_attr = html.escape(safe_js_string)
3103
+
3104
+ # The HTML payload includes inline JS to trigger the browser's native clipboard API
3105
+ # Notice we swapped to double-quotes for the onclick attribute to wrap the escaped entities
3106
+ html_payload = f"""
3107
+ <div style="display: flex; justify-content: flex-end; margin-top: 8px;">
3108
+ <button onclick="navigator.clipboard.writeText({safe_html_attr}).then(() => {{ const original = this.innerHTML; this.innerHTML = '✅ Copied!'; setTimeout(() => this.innerHTML = original, 2000); }}).catch(() => this.innerHTML='❌ Error')"
3109
+ style="display: flex; align-items: center; gap: 6px; background: var(--jp-layout-color2, #f5f5f5); border: 1px solid var(--jp-border-color1, #ccc); border-radius: 4px; padding: 6px 12px; cursor: pointer; color: var(--jp-ui-font-color1, #333); font-family: inherit; font-size: 0.9em; transition: all 0.2s;">
3110
+ {clipboard_svg} {label}
3111
+ </button>
3112
+ </div>
3113
+ """
3114
+ display(HTML(html_payload))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 1.76
3
+ Version: 1.78
4
4
  Summary: A Notebook-inspired Hand-cranked Agentic Framework
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pipulate"
7
7
  # VERSION NOTE: Synced from pipulate/__init__.py.__version__ via python version_sync.py
8
- version = "1.76"
8
+ version = "1.78"
9
9
  authors = [
10
10
  { name="Mike Levin", email="pipulate@gmail.com" },
11
11
  ]
@@ -1,4 +1,4 @@
1
- # Hi, AI! Please run: .venv/bin/python AI_RUNME.py
1
+ #clean Hi, AI! Please run: .venv/bin/python AI_RUNME.py
2
2
 
3
3
  # Pipulate: A hand-cranked, local-first AI SEO software framework.
4
4
  # Copyright (C) 2026 Michael Jay Levin
@@ -1869,6 +1869,16 @@ async def startup_event():
1869
1869
  progressive distillation workflows that define the Pipulate vision.
1870
1870
  """
1871
1871
  pipulate.db['server_start_time'] = str(time.time())
1872
+ # 🧹 THE SLATE WIPE: Clear the greeting sentinel on a true manual start
1873
+ if not os.environ.get('PIPULATE_WATCHDOG_RESTART'):
1874
+ greet_sentinel = pipulate.paths.data / '.has_greeted'
1875
+ try:
1876
+ if greet_sentinel.exists():
1877
+ greet_sentinel.unlink()
1878
+ logger.info("🧹 Slate wipe: Cleared greeting sentinel for new server session.")
1879
+ except Exception as e:
1880
+ logger.warning(f"Could not clear greeting sentinel: {e}")
1881
+
1872
1882
 
1873
1883
  # 🔧 MCP Tools should already be registered from main startup sequence
1874
1884
  # This async startup event handles final initialization only
@@ -2030,11 +2040,13 @@ if __name__ == '__main__':
2030
2040
  )
2031
2041
  slog.safe_print(startup_summary)
2032
2042
  logger.info("🔧 STARTUP_MARKER_3: aa.startup_summary_table displayed")
2043
+ time.sleep(0.05) # 💨 Let the terminal buffer flush
2033
2044
 
2034
2045
  # Show AI capabilities summary
2035
2046
  ai_summary = aa.ai_breadcrumb_summary(tool_count)
2036
2047
  slog.safe_print(ai_summary)
2037
2048
  logger.info("🔧 STARTUP_MARKER_4: ai_breadcrumb_summary displayed")
2049
+ time.sleep(0.05) # 💨 Let the terminal buffer flush
2038
2050
 
2039
2051
  # Show critical environment warnings
2040
2052
  warnings_summary = aa.startup_environment_warnings()
@@ -2239,6 +2251,21 @@ def create_nav_menu():
2239
2251
  # Search container with dropdown results
2240
2252
  search_results_dropdown = Div(id='search-results-dropdown', cls='search-dropdown', role='listbox', aria_label='Search results')
2241
2253
 
2254
+ # Inside create_nav_menu()
2255
+ voice_is_on = pipulate.db.get('voice_enabled', '0') == '1'
2256
+ voice_icon = "🔊" if voice_is_on else "🔇"
2257
+
2258
+ voice_quick_toggle = Button(
2259
+ voice_icon,
2260
+ id="global-voice-toggle",
2261
+ hx_post="/toggle_voice_system",
2262
+ hx_target="#global-voice-toggle",
2263
+ hx_swap="outerHTML",
2264
+ cls="outline secondary",
2265
+ style="border: none; background: transparent; font-size: 1.2rem; padding: 0.2rem 0.5rem; margin-bottom: 0; display: flex; align-items: center;",
2266
+ title="Toggle Voice"
2267
+ )
2268
+
2242
2269
  nav_search_container = Div(
2243
2270
  Input(
2244
2271
  type='search',
@@ -2263,7 +2290,7 @@ def create_nav_menu():
2263
2290
  aria_label='Plugin search'
2264
2291
  )
2265
2292
 
2266
- menus = Div(nav_search_container, create_profile_menu(selected_profile_id, selected_profile_name), create_app_menu(menux), create_env_menu(), poke_section, cls='nav-menu-group')
2293
+ menus = Div(voice_quick_toggle, nav_search_container, create_profile_menu(selected_profile_id, selected_profile_name), create_app_menu(menux), create_env_menu(), poke_section, cls='nav-menu-group')
2267
2294
  nav = Div(breadcrumb, menus, cls='nav-breadcrumb')
2268
2295
  logger.debug('Navigation menu created.')
2269
2296
  return nav
@@ -2872,23 +2899,6 @@ async def poke_flyout(request):
2872
2899
  id='theme-switch-container'
2873
2900
  )
2874
2901
 
2875
- # --- NEW: Voice toggle switch ---
2876
- voice_switch = Div(
2877
- Label(
2878
- Input(
2879
- type='checkbox',
2880
- role='switch',
2881
- name='voice_switch',
2882
- checked=voice_is_on,
2883
- hx_post='/toggle_voice_system',
2884
- hx_target='#voice-switch-container',
2885
- hx_swap='outerHTML'
2886
- ),
2887
- Span(f'{voice_icon} Voice Output', cls='ml-quarter')
2888
- ),
2889
- id='voice-switch-container'
2890
- )
2891
-
2892
2902
  # 🆕 INJECT THE NEW SURGICAL RESET BUTTON HERE
2893
2903
  reset_config_button = Button('⚠️ Reset Config & Onboarding',
2894
2904
  hx_post='/reset-config-onboarding',
@@ -2957,7 +2967,6 @@ async def poke_flyout(request):
2957
2967
  # Build list items in the requested order: Theme Toggle, Lock Profile, Update, Clear Workflows, Reset Database, MCP Test
2958
2968
  list_items = [
2959
2969
  Li(theme_switch, cls='flyout-list-item'),
2960
- Li(voice_switch, cls='flyout-list-item'), # <--- INJECTED HERE
2961
2970
  Li(lock_button, cls='flyout-list-item'),
2962
2971
  Li(update_button, cls='flyout-list-item')
2963
2972
  ]
@@ -3332,29 +3341,21 @@ async def toggle_voice_system(request):
3332
3341
  except Exception as e:
3333
3342
  logger.warning(f"Error silencing voice system: {e}")
3334
3343
 
3335
- # Re-render the switch component to reflect the new state
3344
+ # Re-render the NEW TOP NAV BUTTON to reflect the new state
3336
3345
  voice_is_on = new_state == '1'
3337
3346
  icon = "🔊" if voice_is_on else "🔇"
3338
-
3339
- voice_switch = Div(
3340
- Label(
3341
- Input(
3342
- type='checkbox',
3343
- role='switch',
3344
- name='voice_switch',
3345
- checked=voice_is_on,
3346
- hx_post='/toggle_voice_system',
3347
- hx_target='#voice-switch-container',
3348
- hx_swap='outerHTML'
3349
- ),
3350
- Span(f'{icon} Voice Output', cls='ml-quarter')
3351
- ),
3352
- id='voice-switch-container',
3353
- cls='voice-switch-container'
3347
+ voice_quick_toggle = Button(
3348
+ icon,
3349
+ id="global-voice-toggle",
3350
+ hx_post="/toggle_voice_system",
3351
+ hx_target="#global-voice-toggle",
3352
+ hx_swap="outerHTML",
3353
+ cls="outline secondary",
3354
+ style="border: none; background: transparent; font-size: 1.2rem; padding: 0.2rem 0.5rem; margin-bottom: 0; display: flex; align-items: center;",
3355
+ title="Toggle Voice"
3354
3356
  )
3355
3357
 
3356
- return voice_switch
3357
-
3358
+ return voice_quick_toggle
3358
3359
 
3359
3360
  @rt('/sync_theme', methods=['POST'])
3360
3361
  async def sync_theme(request):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes