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.
- {pipulate-1.76/pipulate.egg-info → pipulate-1.78}/PKG-INFO +1 -1
- {pipulate-1.76 → pipulate-1.78}/__init__.py +2 -2
- {pipulate-1.76 → pipulate-1.78}/apps/010_introduction.py +50 -50
- {pipulate-1.76 → pipulate-1.78}/imports/voice_synthesis.py +5 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate/core.py +32 -0
- {pipulate-1.76 → pipulate-1.78/pipulate.egg-info}/PKG-INFO +1 -1
- {pipulate-1.76 → pipulate-1.78}/pyproject.toml +1 -1
- {pipulate-1.76 → pipulate-1.78}/server.py +40 -39
- {pipulate-1.76 → pipulate-1.78}/LICENSE +0 -0
- {pipulate-1.76 → pipulate-1.78}/README.md +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/015_config.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/020_profiles.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/025_aspect.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/030_roles.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/040_hello_workflow.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/050_documentation.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/060_tasks.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/070_history.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/100_connect_with_botify.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/110_parameter_buster.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/120_link_graph.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/130_gap_analysis.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/200_workflow_genesis.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/210_widget_examples.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/220_roadmap.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/230_dev_assistant.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/240_simon_mcp.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/300_blank_placeholder.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/400_botify_trifecta.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/440_browser_automation.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/450_stream_simulator.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/510_text_field.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/520_text_area.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/530_dropdown.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/540_checkboxes.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/550_radios.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/560_range.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/570_switch.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/580_upload.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/610_markdown.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/620_mermaid.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/630_prism.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/640_javascript.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/710_pandas.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/720_rich.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/730_matplotlib.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/810_webbrowser.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/820_selenium.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/apps/xx_link_graph_v2.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/cli.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/config.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/__init__.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/ai_dictdb.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/append_only_conversation.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/ascii_displays.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/botify/__init__.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/botify/code_generators.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/botify_code_generation.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/crud.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/database_safety_wrapper.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/__init__.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/durable_backup_system.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/mcp_orchestrator.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/server_logging.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/imports/stream_orchestrator.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate/__init__.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate/pipulate.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-1.76 → pipulate-1.78}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-1.76 → pipulate-1.78}/setup.cfg +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/__init__.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/advanced_automation_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/botify_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/conversation_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/dom_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/keychain_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/llm_optics.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/mcp_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/scraper_tools.py +0 -0
- {pipulate-1.76 → pipulate-1.78}/tools/system_tools.py +0 -0
|
@@ -12,8 +12,8 @@ Usage:
|
|
|
12
12
|
pipulate
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
__version__ = "1.
|
|
16
|
-
__version_description__ = "
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
Div(
|
|
195
|
+
onload_trigger,
|
|
196
|
+
Card(
|
|
193
197
|
Div(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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))
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
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
|
|
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
|
|
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
|