diff --git a/.pylint_cache/src_1.stats b/.pylint_cache/src_1.stats new file mode 100644 index 0000000..36012ce Binary files /dev/null and b/.pylint_cache/src_1.stats differ diff --git a/src/autometabuilder/messages_en.json b/src/autometabuilder/messages_en.json index 481beca..61668a4 100644 --- a/src/autometabuilder/messages_en.json +++ b/src/autometabuilder/messages_en.json @@ -368,5 +368,22 @@ "ui.workflow.run_when_placeholder": "$flag_key", "ui.workflow.loop_body_label": "Loop Body", "ui.workflow.add_loop_node": "Add Node to Loop", - "ui.workflow.delete_node": "Delete node" + "ui.workflow.delete_node": "Delete node", + "ui.workflow.templates.title": "Workflow Templates", + "ui.workflow.templates.select_label": "Template", + "ui.workflow.templates.select_placeholder": "Select a template...", + "ui.workflow.templates.description_placeholder": "Choose a template to preview what it does.", + "ui.workflow.templates.apply": "Apply Template", + "ui.workflow.templates.confirm_apply": "Replace the current workflow with this template?", + "ui.workflow.templates.loaded": "Template loaded.", + "ui.workflow.templates.error": "Unable to load templates.", + "ui.workflow.templates.error_load": "Unable to load the selected template.", + "meta.workflow_packages.blank.label": "Blank Canvas", + "meta.workflow_packages.blank.description": "Start with an empty workflow.", + "meta.workflow_packages.iterative_loop.label": "Iterative Agent Loop", + "meta.workflow_packages.iterative_loop.description": "Cycle AI planning, tool execution, and feedback until no tool calls remain.", + "meta.workflow_packages.single_pass.label": "Single Pass", + "meta.workflow_packages.single_pass.description": "Run one AI request and tool execution sequence without looping.", + "meta.workflow_packages.plan_execute_summarize.label": "Plan, Execute, Summarize", + "meta.workflow_packages.plan_execute_summarize.description": "Plan with the model, run tools, then ask for a final recap." } diff --git a/src/autometabuilder/messages_es.json b/src/autometabuilder/messages_es.json index 80b7e38..d1904e9 100644 --- a/src/autometabuilder/messages_es.json +++ b/src/autometabuilder/messages_es.json @@ -364,5 +364,22 @@ "ui.workflow.run_when_placeholder": "$flag_key", "ui.workflow.loop_body_label": "Cuerpo del bucle", "ui.workflow.add_loop_node": "Añadir nodo al bucle", - "ui.workflow.delete_node": "Eliminar nodo" + "ui.workflow.delete_node": "Eliminar nodo", + "ui.workflow.templates.title": "Plantillas de flujo de trabajo", + "ui.workflow.templates.select_label": "Plantilla", + "ui.workflow.templates.select_placeholder": "Selecciona una plantilla...", + "ui.workflow.templates.description_placeholder": "Elige una plantilla para ver lo que hace.", + "ui.workflow.templates.apply": "Aplicar plantilla", + "ui.workflow.templates.confirm_apply": "¿Reemplazar el flujo de trabajo actual con esta plantilla?", + "ui.workflow.templates.loaded": "Plantilla cargada.", + "ui.workflow.templates.error": "No se pueden cargar las plantillas.", + "ui.workflow.templates.error_load": "No se puede cargar la plantilla seleccionada.", + "meta.workflow_packages.blank.label": "Lienzo en blanco", + "meta.workflow_packages.blank.description": "Empieza con un flujo de trabajo vacío.", + "meta.workflow_packages.iterative_loop.label": "Bucle iterativo del agente", + "meta.workflow_packages.iterative_loop.description": "Cicla planificación de IA, ejecución de herramientas y retroalimentación hasta que no haya llamadas a herramientas.", + "meta.workflow_packages.single_pass.label": "Una sola pasada", + "meta.workflow_packages.single_pass.description": "Ejecuta una solicitud de IA y la secuencia de herramientas una vez, sin bucle.", + "meta.workflow_packages.plan_execute_summarize.label": "Planear, ejecutar, resumir", + "meta.workflow_packages.plan_execute_summarize.description": "Planifica con el modelo, ejecuta herramientas y luego pide un resumen final." } diff --git a/src/autometabuilder/messages_fr.json b/src/autometabuilder/messages_fr.json index a6abec2..9f6c13f 100644 --- a/src/autometabuilder/messages_fr.json +++ b/src/autometabuilder/messages_fr.json @@ -364,5 +364,22 @@ "ui.workflow.run_when_placeholder": "$flag_key", "ui.workflow.loop_body_label": "Corps de la boucle", "ui.workflow.add_loop_node": "Ajouter un nœud à la boucle", - "ui.workflow.delete_node": "Supprimer le nœud" + "ui.workflow.delete_node": "Supprimer le nœud", + "ui.workflow.templates.title": "Modèles de flux de travail", + "ui.workflow.templates.select_label": "Modèle", + "ui.workflow.templates.select_placeholder": "Sélectionnez un modèle...", + "ui.workflow.templates.description_placeholder": "Choisissez un modèle pour voir ce qu'il fait.", + "ui.workflow.templates.apply": "Appliquer le modèle", + "ui.workflow.templates.confirm_apply": "Remplacer le flux de travail actuel par ce modèle ?", + "ui.workflow.templates.loaded": "Modèle chargé.", + "ui.workflow.templates.error": "Impossible de charger les modèles.", + "ui.workflow.templates.error_load": "Impossible de charger le modèle sélectionné.", + "meta.workflow_packages.blank.label": "Toile vierge", + "meta.workflow_packages.blank.description": "Commencez avec un workflow vide.", + "meta.workflow_packages.iterative_loop.label": "Boucle itérative de l'agent", + "meta.workflow_packages.iterative_loop.description": "Itère la planification IA, l'exécution des outils et les retours jusqu'à ce qu'il n'y ait plus d'appels d'outils.", + "meta.workflow_packages.single_pass.label": "Passe unique", + "meta.workflow_packages.single_pass.description": "Exécute une requête IA et une séquence d'outils une seule fois, sans boucle.", + "meta.workflow_packages.plan_execute_summarize.label": "Planifier, exécuter, résumer", + "meta.workflow_packages.plan_execute_summarize.description": "Planifiez avec le modèle, exécutez les outils, puis demandez un récapitulatif final." } diff --git a/src/autometabuilder/messages_nl.json b/src/autometabuilder/messages_nl.json index 0dc9b9b..18c23c0 100644 --- a/src/autometabuilder/messages_nl.json +++ b/src/autometabuilder/messages_nl.json @@ -364,5 +364,22 @@ "ui.workflow.run_when_placeholder": "$flag_key", "ui.workflow.loop_body_label": "Lusinhoud", "ui.workflow.add_loop_node": "Node aan lus toevoegen", - "ui.workflow.delete_node": "Node verwijderen" + "ui.workflow.delete_node": "Node verwijderen", + "ui.workflow.templates.title": "Workflow-sjablonen", + "ui.workflow.templates.select_label": "Sjabloon", + "ui.workflow.templates.select_placeholder": "Selecteer een sjabloon...", + "ui.workflow.templates.description_placeholder": "Kies een sjabloon om te zien wat het doet.", + "ui.workflow.templates.apply": "Sjabloon toepassen", + "ui.workflow.templates.confirm_apply": "Het huidige workflow vervangen door dit sjabloon?", + "ui.workflow.templates.loaded": "Sjabloon geladen.", + "ui.workflow.templates.error": "Kan sjablonen niet laden.", + "ui.workflow.templates.error_load": "Kan het geselecteerde sjabloon niet laden.", + "meta.workflow_packages.blank.label": "Leeg canvas", + "meta.workflow_packages.blank.description": "Begin met een lege workflow.", + "meta.workflow_packages.iterative_loop.label": "Iteratieve agentlus", + "meta.workflow_packages.iterative_loop.description": "Herhaalt AI-planning, tooluitvoering en feedback tot er geen tool-aanroepen meer zijn.", + "meta.workflow_packages.single_pass.label": "Enkele run", + "meta.workflow_packages.single_pass.description": "Voer een AI-aanvraag en toolreeks een keer uit, zonder lus.", + "meta.workflow_packages.plan_execute_summarize.label": "Plan, voer uit, vat samen", + "meta.workflow_packages.plan_execute_summarize.description": "Plan met het model, voer tools uit en vraag daarna om een eindoverzicht." } diff --git a/src/autometabuilder/messages_pirate.json b/src/autometabuilder/messages_pirate.json index ffe27b8..7e701e6 100644 --- a/src/autometabuilder/messages_pirate.json +++ b/src/autometabuilder/messages_pirate.json @@ -364,5 +364,22 @@ "ui.workflow.run_when_placeholder": "$flag_key", "ui.workflow.loop_body_label": "Loop Body", "ui.workflow.add_loop_node": "Add Node to Loop", - "ui.workflow.delete_node": "Delete node" + "ui.workflow.delete_node": "Delete node", + "ui.workflow.templates.title": "Workflow Treasure Maps", + "ui.workflow.templates.select_label": "Template", + "ui.workflow.templates.select_placeholder": "Pick a template...", + "ui.workflow.templates.description_placeholder": "Pick a template to see what it does.", + "ui.workflow.templates.apply": "Hoist Template", + "ui.workflow.templates.confirm_apply": "Replace the current workflow with this template?", + "ui.workflow.templates.loaded": "Template loaded.", + "ui.workflow.templates.error": "Can't load templates.", + "ui.workflow.templates.error_load": "Can't load the selected template.", + "meta.workflow_packages.blank.label": "Blank Deck", + "meta.workflow_packages.blank.description": "Start with an empty workflow.", + "meta.workflow_packages.iterative_loop.label": "Iterative Matey Loop", + "meta.workflow_packages.iterative_loop.description": "Cycle AI planning, tool work, and feedback till there be no tool calls.", + "meta.workflow_packages.single_pass.label": "Single Pass", + "meta.workflow_packages.single_pass.description": "Run one AI request and tool run, no loop.", + "meta.workflow_packages.plan_execute_summarize.label": "Plan, Plunder, Summarize", + "meta.workflow_packages.plan_execute_summarize.description": "Plan with the model, run tools, then ask for a final recap." } diff --git a/src/autometabuilder/metadata.json b/src/autometabuilder/metadata.json index e96e00a..a54a0b3 100644 --- a/src/autometabuilder/metadata.json +++ b/src/autometabuilder/metadata.json @@ -1,6 +1,7 @@ { "tools_path": "tools.json", "workflow_path": "workflow.json", + "workflow_packages_path": "workflow_packages", "messages": { "en": "messages_en.json", "es": "messages_es.json", diff --git a/src/autometabuilder/web/server.py b/src/autometabuilder/web/server.py index bc4f8e9..ac4bde3 100644 --- a/src/autometabuilder/web/server.py +++ b/src/autometabuilder/web/server.py @@ -216,6 +216,35 @@ def get_workflow_content(): with open(workflow_path, "r", encoding="utf-8") as f: return f.read() +def get_workflow_packages_dir(): + pkg_dir = os.path.dirname(os.path.dirname(__file__)) + metadata = get_metadata() + packages_dir = metadata.get("workflow_packages_path", "workflow_packages") + return os.path.join(pkg_dir, packages_dir) + +def load_workflow_packages(): + packages_dir = get_workflow_packages_dir() + if not os.path.isdir(packages_dir): + return [] + packages = [] + for filename in sorted(os.listdir(packages_dir)): + if not filename.endswith(".json"): + continue + path = os.path.join(packages_dir, filename) + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError: + continue + if not isinstance(data, dict): + continue + package_id = data.get("id") or os.path.splitext(filename)[0] + data["id"] = package_id + if "workflow" not in data: + data["workflow"] = {"nodes": []} + packages.append(data) + return packages + @app.get("/", response_class=HTMLResponse) async def read_item(request: Request, username: str = Depends(get_current_user)): logs = get_recent_logs() @@ -318,6 +347,27 @@ async def get_workflow_plugins(username: str = Depends(get_current_user)): metadata = get_metadata() return metadata.get("workflow_plugins", {}) +@app.get("/api/workflow/packages", response_class=JSONResponse) +async def list_workflow_packages(username: str = Depends(get_current_user)): + packages = load_workflow_packages() + summarized = [] + for package in packages: + summarized.append({ + "id": package.get("id"), + "label": package.get("label", ""), + "description": package.get("description", ""), + "tags": package.get("tags", []) + }) + return {"packages": summarized} + +@app.get("/api/workflow/packages/{package_id}", response_class=JSONResponse) +async def get_workflow_package(package_id: str, username: str = Depends(get_current_user)): + packages = load_workflow_packages() + for package in packages: + if package.get("id") == package_id: + return package + raise HTTPException(status_code=404, detail="Workflow package not found") + @app.get("/api/logs") async def get_logs(username: str = Depends(get_current_user)): return {"logs": get_recent_logs()} diff --git a/src/autometabuilder/web/static/js/index.js b/src/autometabuilder/web/static/js/index.js index 302903c..4241148 100644 --- a/src/autometabuilder/web/static/js/index.js +++ b/src/autometabuilder/web/static/js/index.js @@ -7,13 +7,120 @@ const format = (text, values = {}) => text.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? ''); const fetchWorkflowPlugins = async () => { - const response = await fetch('/api/workflow/plugins'); + const response = await fetch('/api/workflow/plugins', { credentials: 'include' }); if (!response.ok) { throw new Error(`Plugin fetch failed: ${response.status}`); } return response.json(); }; + const fetchWorkflowPackages = async () => { + const response = await fetch('/api/workflow/packages', { credentials: 'include' }); + if (!response.ok) { + throw new Error(`Package fetch failed: ${response.status}`); + } + return response.json(); + }; + + const WorkflowTemplates = { + packages: [], + selectEl: null, + descEl: null, + applyBtn: null, + + async init() { + this.selectEl = document.getElementById('workflow-template-select'); + this.descEl = document.getElementById('workflow-template-description'); + this.applyBtn = document.getElementById('workflow-template-apply'); + if (!this.selectEl) return; + + try { + const data = await fetchWorkflowPackages(); + this.packages = data.packages || []; + this.renderOptions(); + this.updateDescription(); + } catch (error) { + console.error('Workflow template fetch failed', error); + if (this.descEl) { + this.descEl.textContent = t('ui.workflow.templates.error', 'Unable to load templates.'); + } + return; + } + + this.selectEl.addEventListener('change', () => this.updateDescription()); + this.applyBtn?.addEventListener('click', () => this.applySelected()); + }, + + renderOptions() { + if (!this.selectEl) return; + this.selectEl.innerHTML = ''; + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = t('ui.workflow.templates.select_placeholder', 'Select a template...'); + placeholder.disabled = true; + placeholder.selected = true; + this.selectEl.appendChild(placeholder); + + this.packages.forEach(pkg => { + const option = document.createElement('option'); + option.value = pkg.id; + option.textContent = t(pkg.label || pkg.id, pkg.label || pkg.id); + this.selectEl.appendChild(option); + }); + }, + + updateDescription() { + if (!this.descEl) return; + const selected = this.getSelectedPackage(); + if (this.applyBtn) { + this.applyBtn.disabled = !selected; + } + if (!selected) { + this.descEl.textContent = t( + 'ui.workflow.templates.description_placeholder', + 'Choose a template to preview what it does.' + ); + return; + } + const description = selected.description || ''; + this.descEl.textContent = t(description, description); + }, + + getSelectedPackage() { + if (!this.selectEl) return null; + const selectedId = this.selectEl.value; + return this.packages.find(pkg => pkg.id === selectedId) || null; + }, + + async applySelected() { + const selected = this.getSelectedPackage(); + if (!selected) return; + const confirmText = t( + 'ui.workflow.templates.confirm_apply', + 'Replace the current workflow with this template?' + ); + if (!confirm(confirmText)) return; + + try { + const response = await fetch(`/api/workflow/packages/${selected.id}`, { credentials: 'include' }); + if (!response.ok) { + throw new Error(`Template fetch failed: ${response.status}`); + } + const data = await response.json(); + const workflow = data.workflow || data; + if (window.WorkflowBuilder && typeof window.WorkflowBuilder.loadWorkflow === 'function') { + window.WorkflowBuilder.loadWorkflow(workflow); + } + if (window.Toast) { + Toast.show(t('ui.workflow.templates.loaded', 'Template loaded.'), 'success'); + } + } catch (error) { + console.error('Workflow template load failed', error); + alert(t('ui.workflow.templates.error_load', 'Unable to load the selected template.')); + } + } + }; + const initWorkflowBuilder = (pluginDefinitions) => { if (!window.WorkflowBuilder) return; const container = document.getElementById('workflow-builder'); @@ -367,6 +474,7 @@ model: ${model} } catch (error) { console.error('Workflow builder failed to initialize', error); } + await WorkflowTemplates.init(); wireRunModeToggles(); wirePromptChips(); }; diff --git a/src/autometabuilder/web/static/js/workflow.js b/src/autometabuilder/web/static/js/workflow.js index 9e3d968..e78eccb 100644 --- a/src/autometabuilder/web/static/js/workflow.js +++ b/src/autometabuilder/web/static/js/workflow.js @@ -362,6 +362,15 @@ const WorkflowBuilder = { } }, + loadWorkflow(workflow) { + if (!workflow || !Array.isArray(workflow.nodes)) { + this.workflow = { nodes: [] }; + } else { + this.workflow = workflow; + } + this.render(); + }, + escapeHtml(text) { if (text === null || text === undefined) return ''; const div = document.createElement('div'); diff --git a/src/autometabuilder/web/templates/base.html b/src/autometabuilder/web/templates/base.html index 712d4c0..c03b839 100644 --- a/src/autometabuilder/web/templates/base.html +++ b/src/autometabuilder/web/templates/base.html @@ -20,40 +20,15 @@ {% block head %}{% endblock %} - - + {% from "components/organisms/sidebar.html" import sidebar %} + {% set nav_items = [ + {'section': 'dashboard', 'icon': 'speedometer2', 'label': t('ui.nav.dashboard', 'Dashboard'), 'active': True}, + {'section': 'workflow', 'icon': 'diagram-3', 'label': t('ui.nav.workflow', 'Workflow'), 'active': False}, + {'section': 'prompt', 'icon': 'file-text', 'label': t('ui.nav.prompt', 'Prompt'), 'active': False}, + {'section': 'settings', 'icon': 'gear', 'label': t('ui.nav.settings', 'Settings'), 'active': False}, + {'section': 'translations', 'icon': 'translate', 'label': t('ui.nav.translations', 'Translations'), 'active': False} + ] %} + {{ sidebar(nav_items, t('ui.app.name', 'AutoMetabuilder'), username, t('ui.theme_toggle', 'Toggle theme')) }}
diff --git a/src/autometabuilder/web/templates/components/atoms/nav_link.html b/src/autometabuilder/web/templates/components/atoms/nav_link.html new file mode 100644 index 0000000..6543546 --- /dev/null +++ b/src/autometabuilder/web/templates/components/atoms/nav_link.html @@ -0,0 +1,6 @@ +{# Sidebar navigation link macro. #} +{% macro nav_link(section, icon_name, label, is_active=False) -%} + + {{ label }} + +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html b/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html new file mode 100644 index 0000000..5a3d626 --- /dev/null +++ b/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html @@ -0,0 +1,10 @@ +{# Sidebar navigation list macro. #} +{% from "components/atoms/nav_link.html" import nav_link %} + +{% macro sidebar_nav(items) -%} + +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/prompt_builder.html b/src/autometabuilder/web/templates/components/organisms/prompt_builder.html new file mode 100644 index 0000000..a74680e --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/prompt_builder.html @@ -0,0 +1,71 @@ +{# Prompt builder card macro. #} +{% from "components/atoms/helper_text.html" import helper_text %} +{% from "components/molecules/card.html" import card %} +{% from "components/sections/prompt_step.html" import prompt_step %} + +{% macro prompt_builder() -%} +{% call card(t('ui.prompt.card.title', 'Prompt Builder'), "chat-square-text") %} +
+ +
+ {% set system_chips = [ + {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.senior.snippet', 'You are a senior software engineer focused on correctness and clarity.'), 'label': t('ui.prompt.chip.senior.label', 'Senior engineer')}, + {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.ask.snippet', 'Ask clarifying questions before making risky changes.'), 'label': t('ui.prompt.chip.ask.label', 'Ask questions')}, + {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.minimal.snippet', 'Prefer minimal diffs and explain trade-offs.'), 'label': t('ui.prompt.chip.minimal.label', 'Minimal diffs')} + ] %} + {{ prompt_step(1, + t('ui.prompt.step1.title', 'Define the assistant'), + t('ui.prompt.step1.desc', 'Describe who the assistant is and how it should behave.'), + 'system-prompt', + 'system_content', + t('ui.prompt.step1.placeholder', 'You are a senior software engineer who prefers small, safe changes.'), + (prompt_content | extract_system_content), + 6, + system_chips + ) }} + + {% set user_chips = [ + {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.ux.snippet', 'Focus on UX polish, and avoid major refactors.'), 'label': t('ui.prompt.chip.ux.label', 'UX polish')}, + {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.tests.snippet', 'Add tests when possible, but avoid heavy scaffolding.'), 'label': t('ui.prompt.chip.tests.label', 'Add tests')}, + {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.summarize.snippet', 'Summarize changes and suggest next steps.'), 'label': t('ui.prompt.chip.summarize.label', 'Summarize')} + ] %} + {{ prompt_step(2, + t('ui.prompt.step2.title', 'Give the mission'), + t('ui.prompt.step2.desc', 'Explain what the bot should accomplish right now.'), + 'user-prompt', + 'user_content', + t('ui.prompt.step2.placeholder', 'Review the repo, improve the UI, and summarize what changed.'), + (prompt_content | extract_user_content), + 5, + user_chips + ) }} + +
+ + {{ helper_text(t('ui.prompt.model.desc', 'Pick the balance of quality and speed that fits the task.')) }} + +
+
+ +
+ + {{ helper_text(t('ui.prompt.raw.desc', 'Edit the full YAML only if you need fine control.')) }} + +
+ +
+ + +
+
+{% endcall %} +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/prompt_guidance.html b/src/autometabuilder/web/templates/components/organisms/prompt_guidance.html new file mode 100644 index 0000000..c9cbedd --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/prompt_guidance.html @@ -0,0 +1,21 @@ +{# Prompt guidance card macro. #} +{% from "components/molecules/card.html" import card %} + +{% macro prompt_guidance() -%} +{% call card(t('ui.prompt.guidance.title', 'Guidance'), "lightbulb") %} +
+
+ {{ t('ui.prompt.guidance.keep_human.title', 'Keep it human') }} +

{{ t('ui.prompt.guidance.keep_human.desc', 'Write instructions the way you would brief a teammate.') }}

+
+
+ {{ t('ui.prompt.guidance.be_specific.title', 'Be specific') }} +

{{ t('ui.prompt.guidance.be_specific.desc', 'Mention constraints like time, scope, or testing expectations.') }}

+
+
+ {{ t('ui.prompt.guidance.raw.title', 'Use advanced YAML sparingly') }} +

{{ t('ui.prompt.guidance.raw.desc', 'Only switch to raw YAML if you need full control.') }}

+
+
+{% endcall %} +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/settings_api_keys.html b/src/autometabuilder/web/templates/components/organisms/settings_api_keys.html new file mode 100644 index 0000000..1d30df9 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/settings_api_keys.html @@ -0,0 +1,31 @@ +{# Settings API keys card macro. #} +{% macro settings_api_keys() -%} +{% set settings_desc = metadata.get('settings_descriptions', {}) %} +
+
+
{{ t('ui.settings.api_keys', 'API Keys') }}
+
+
+ {% for key in ['GITHUB_TOKEN', 'OPENAI_API_KEY', 'LITELLM_API_KEY'] %} + {% set desc = settings_desc.get(key, {}) %} + {% set desc_label = desc.get('label', key) %} + {% set desc_text = desc.get('description', '') %} + {% set desc_placeholder = desc.get('placeholder', '') %} +
+ + {% if desc_text %} +

{{ t(desc_text, desc_text) }}

+ {% endif %} + +
+ {% endfor %} +
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/settings_configuration.html b/src/autometabuilder/web/templates/components/organisms/settings_configuration.html new file mode 100644 index 0000000..e0df7f1 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/settings_configuration.html @@ -0,0 +1,37 @@ +{# Settings configuration card macro. #} +{% macro settings_configuration() -%} +{% set settings_desc = metadata.get('settings_descriptions', {}) %} +
+
+
{{ t('ui.settings.configuration', 'Configuration') }}
+
+
+ {% for key in ['GITHUB_REPOSITORY', 'LOG_LEVEL', 'APP_LANG', 'PROMPT_PATH'] %} + {% set desc = settings_desc.get(key, {}) %} + {% set desc_label = desc.get('label', key) %} + {% set desc_text = desc.get('description', '') %} + {% set desc_placeholder = desc.get('placeholder', '') %} +
+ + {% if desc_text %} +

{{ t(desc_text, desc_text) }}

+ {% endif %} + {% if desc.get('type') == 'select' %} + + {% else %} + + {% endif %} +
+ {% endfor %} +
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/settings_other.html b/src/autometabuilder/web/templates/components/organisms/settings_other.html new file mode 100644 index 0000000..75dfa09 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/settings_other.html @@ -0,0 +1,44 @@ +{# Settings other settings card macro. #} +{% macro settings_other() -%} +{% set settings_desc = metadata.get('settings_descriptions', {}) %} +{% set known_keys = ['GITHUB_TOKEN', 'OPENAI_API_KEY', 'LITELLM_API_KEY', 'GITHUB_REPOSITORY', 'LOG_LEVEL', 'APP_LANG', 'PROMPT_PATH', 'WEB_USER', 'WEB_PASSWORD'] %} +
+
+
{{ t('ui.settings.other', 'Other Settings') }}
+
+
+
+ {% for key, value in env_vars.items() %} + {% if key not in known_keys %} +
+
+ {% set desc = settings_desc.get(key, {}) %} + {% set desc_label = desc.get('label', key) %} + {% set desc_text = desc.get('description', t('ui.settings.custom_default_desc', 'Custom environment setting. Add a description in metadata.json to show it here.')) %} + +

{{ t(desc_text, desc_text) }}

+ +
+
+ {% endif %} + {% endfor %} +
+
+ +

{{ t('ui.settings.add.desc', 'Use uppercase keys with underscores, like API_TIMEOUT.') }}

+
+ + +
+
+
+
+
+ +
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/settings_web_access.html b/src/autometabuilder/web/templates/components/organisms/settings_web_access.html new file mode 100644 index 0000000..1027bd7 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/settings_web_access.html @@ -0,0 +1,26 @@ +{# Settings web access card macro. #} +{% macro settings_web_access() -%} +{% set settings_desc = metadata.get('settings_descriptions', {}) %} +
+
+
{{ t('ui.settings.web_access', 'Web UI Access') }}
+
+
+ {% for key in ['WEB_USER', 'WEB_PASSWORD'] %} + {% set desc = settings_desc.get(key, {}) %} + {% set desc_label = desc.get('label', key) %} + {% set desc_text = desc.get('description', '') %} +
+ + {% if desc_text %} +

{{ t(desc_text, desc_text) }}

+ {% endif %} + +
+ {% endfor %} +
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/sidebar.html b/src/autometabuilder/web/templates/components/organisms/sidebar.html new file mode 100644 index 0000000..d8c0ef5 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/sidebar.html @@ -0,0 +1,12 @@ +{# Sidebar macro. #} +{% from "components/organisms/sidebar_header.html" import sidebar_header %} +{% from "components/molecules/sidebar_nav.html" import sidebar_nav %} +{% from "components/organisms/sidebar_footer.html" import sidebar_footer %} + +{% macro sidebar(nav_items, app_name, username, toggle_title) -%} + +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/sidebar_footer.html b/src/autometabuilder/web/templates/components/organisms/sidebar_footer.html new file mode 100644 index 0000000..891cd89 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/sidebar_footer.html @@ -0,0 +1,12 @@ +{# Sidebar footer macro. #} +{% macro sidebar_footer(username, toggle_title) -%} + +{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/sidebar_header.html b/src/autometabuilder/web/templates/components/organisms/sidebar_header.html new file mode 100644 index 0000000..5ee546d --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/sidebar_header.html @@ -0,0 +1,6 @@ +{# Sidebar header macro. #} +{% macro sidebar_header(app_name) -%} +
+
{{ app_name }}
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/translations_editor.html b/src/autometabuilder/web/templates/components/organisms/translations_editor.html new file mode 100644 index 0000000..c497245 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/translations_editor.html @@ -0,0 +1,62 @@ +{# Translations editor card macro. #} +{% from "components/molecules/empty_state.html" import empty_state %} + +{% macro translations_editor() -%} +
+
+
{{ t('ui.translations.editor.title', 'Translation Editor') }}
+ +
+
+
+ {{ empty_state("translate", t('ui.translations.empty.title', 'Pick a language'), t('ui.translations.empty.body', 'Select a language from the list to start editing translations.')) }} +
+ +
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/translations_languages.html b/src/autometabuilder/web/templates/components/organisms/translations_languages.html new file mode 100644 index 0000000..e3d7017 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/translations_languages.html @@ -0,0 +1,48 @@ +{# Translations languages card macro. #} +{% macro translations_languages() -%} +
+
+
{{ t('ui.translations.languages', 'Languages') }}
+
+
+
+ {% for lang, file in translations.items() %} +
+
+ +
+ {{ lang.upper() }} + {{ file }} +
+
+
+ + {% if lang != 'en' %} + + {% endif %} +
+
+ {% endfor %} +
+
+ +
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/organisms/workflow_templates.html b/src/autometabuilder/web/templates/components/organisms/workflow_templates.html new file mode 100644 index 0000000..b7d643f --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/workflow_templates.html @@ -0,0 +1,30 @@ +{# Workflow templates card macro. #} +{% macro workflow_templates() -%} +
+
+
{{ t('ui.workflow.templates.title', 'Workflow Templates') }}
+
+
+
+
+ + +
+
+
+ {{ t('ui.workflow.templates.description_placeholder', 'Choose a template to preview what it does.') }} +
+
+
+ +
+
+
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/sections/prompt_section.html b/src/autometabuilder/web/templates/components/sections/prompt_section.html index 957c519..db037a1 100644 --- a/src/autometabuilder/web/templates/components/sections/prompt_section.html +++ b/src/autometabuilder/web/templates/components/sections/prompt_section.html @@ -1,8 +1,7 @@ {# Prompt section macro. #} {% from "components/atoms/section_header.html" import section_header %} -{% from "components/atoms/helper_text.html" import helper_text %} -{% from "components/molecules/card.html" import card %} -{% from "components/sections/prompt_step.html" import prompt_step %} +{% from "components/organisms/prompt_builder.html" import prompt_builder with context %} +{% from "components/organisms/prompt_guidance.html" import prompt_guidance with context %} {% macro prompt_section() -%}
@@ -10,89 +9,11 @@
- {% call card(t('ui.prompt.card.title', 'Prompt Builder'), "chat-square-text") %} -
- -
- {% set system_chips = [ - {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.senior.snippet', 'You are a senior software engineer focused on correctness and clarity.'), 'label': t('ui.prompt.chip.senior.label', 'Senior engineer')}, - {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.ask.snippet', 'Ask clarifying questions before making risky changes.'), 'label': t('ui.prompt.chip.ask.label', 'Ask questions')}, - {'target': 'system-prompt', 'snippet': t('ui.prompt.chip.minimal.snippet', 'Prefer minimal diffs and explain trade-offs.'), 'label': t('ui.prompt.chip.minimal.label', 'Minimal diffs')} - ] %} - {{ prompt_step(1, - t('ui.prompt.step1.title', 'Define the assistant'), - t('ui.prompt.step1.desc', 'Describe who the assistant is and how it should behave.'), - 'system-prompt', - 'system_content', - t('ui.prompt.step1.placeholder', 'You are a senior software engineer who prefers small, safe changes.'), - (prompt_content | extract_system_content), - 6, - system_chips - ) }} - - {% set user_chips = [ - {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.ux.snippet', 'Focus on UX polish, and avoid major refactors.'), 'label': t('ui.prompt.chip.ux.label', 'UX polish')}, - {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.tests.snippet', 'Add tests when possible, but avoid heavy scaffolding.'), 'label': t('ui.prompt.chip.tests.label', 'Add tests')}, - {'target': 'user-prompt', 'snippet': t('ui.prompt.chip.summarize.snippet', 'Summarize changes and suggest next steps.'), 'label': t('ui.prompt.chip.summarize.label', 'Summarize')} - ] %} - {{ prompt_step(2, - t('ui.prompt.step2.title', 'Give the mission'), - t('ui.prompt.step2.desc', 'Explain what the bot should accomplish right now.'), - 'user-prompt', - 'user_content', - t('ui.prompt.step2.placeholder', 'Review the repo, improve the UI, and summarize what changed.'), - (prompt_content | extract_user_content), - 5, - user_chips - ) }} - -
- - {{ helper_text(t('ui.prompt.model.desc', 'Pick the balance of quality and speed that fits the task.')) }} - -
-
- -
- - {{ helper_text(t('ui.prompt.raw.desc', 'Edit the full YAML only if you need fine control.')) }} - -
- -
- - -
-
- {% endcall %} + {{ prompt_builder() }}
- {% call card(t('ui.prompt.guidance.title', 'Guidance'), "lightbulb") %} -
-
- {{ t('ui.prompt.guidance.keep_human.title', 'Keep it human') }} -

{{ t('ui.prompt.guidance.keep_human.desc', 'Write instructions the way you would brief a teammate.') }}

-
-
- {{ t('ui.prompt.guidance.be_specific.title', 'Be specific') }} -

{{ t('ui.prompt.guidance.be_specific.desc', 'Mention constraints like time, scope, or testing expectations.') }}

-
-
- {{ t('ui.prompt.guidance.raw.title', 'Use advanced YAML sparingly') }} -

{{ t('ui.prompt.guidance.raw.desc', 'Only switch to raw YAML if you need full control.') }}

-
-
- {% endcall %} + {{ prompt_guidance() }}
diff --git a/src/autometabuilder/web/templates/components/sections/settings_section.html b/src/autometabuilder/web/templates/components/sections/settings_section.html index d1de71d..99d63c4 100644 --- a/src/autometabuilder/web/templates/components/sections/settings_section.html +++ b/src/autometabuilder/web/templates/components/sections/settings_section.html @@ -1,6 +1,10 @@ {# Settings section macro. #} {% from "components/atoms/section_header.html" import section_header %} {% from "components/organisms/callout.html" import callout %} +{% from "components/organisms/settings_api_keys.html" import settings_api_keys with context %} +{% from "components/organisms/settings_configuration.html" import settings_configuration with context %} +{% from "components/organisms/settings_web_access.html" import settings_web_access with context %} +{% from "components/organisms/settings_other.html" import settings_other with context %} {% macro settings_section() -%}
@@ -10,138 +14,17 @@
- {% set settings_desc = metadata.get('settings_descriptions', {}) %} -
-
-
-
{{ t('ui.settings.api_keys', 'API Keys') }}
-
-
- {% for key in ['GITHUB_TOKEN', 'OPENAI_API_KEY', 'LITELLM_API_KEY'] %} - {% set desc = settings_desc.get(key, {}) %} - {% set desc_label = desc.get('label', key) %} - {% set desc_text = desc.get('description', '') %} - {% set desc_placeholder = desc.get('placeholder', '') %} -
- - {% if desc_text %} -

{{ t(desc_text, desc_text) }}

- {% endif %} - -
- {% endfor %} -
-
+ {{ settings_api_keys() }}
-
-
-
{{ t('ui.settings.configuration', 'Configuration') }}
-
-
- {% for key in ['GITHUB_REPOSITORY', 'LOG_LEVEL', 'APP_LANG', 'PROMPT_PATH'] %} - {% set desc = settings_desc.get(key, {}) %} - {% set desc_label = desc.get('label', key) %} - {% set desc_text = desc.get('description', '') %} - {% set desc_placeholder = desc.get('placeholder', '') %} -
- - {% if desc_text %} -

{{ t(desc_text, desc_text) }}

- {% endif %} - {% if desc.get('type') == 'select' %} - - {% else %} - - {% endif %} -
- {% endfor %} -
-
- -
-
-
{{ t('ui.settings.web_access', 'Web UI Access') }}
-
-
- {% for key in ['WEB_USER', 'WEB_PASSWORD'] %} - {% set desc = settings_desc.get(key, {}) %} - {% set desc_label = desc.get('label', key) %} - {% set desc_text = desc.get('description', '') %} -
- - {% if desc_text %} -

{{ t(desc_text, desc_text) }}

- {% endif %} - -
- {% endfor %} -
-
+ {{ settings_configuration() }} + {{ settings_web_access() }}
-
-
-
{{ t('ui.settings.other', 'Other Settings') }}
-
-
-
- {% set known_keys = ['GITHUB_TOKEN', 'OPENAI_API_KEY', 'LITELLM_API_KEY', 'GITHUB_REPOSITORY', 'LOG_LEVEL', 'APP_LANG', 'PROMPT_PATH', 'WEB_USER', 'WEB_PASSWORD'] %} - {% for key, value in env_vars.items() %} - {% if key not in known_keys %} -
-
- {% set desc = settings_desc.get(key, {}) %} - {% set desc_label = desc.get('label', key) %} - {% set desc_text = desc.get('description', t('ui.settings.custom_default_desc', 'Custom environment setting. Add a description in metadata.json to show it here.')) %} - -

{{ t(desc_text, desc_text) }}

- -
-
- {% endif %} - {% endfor %} -
-
- -

{{ t('ui.settings.add.desc', 'Use uppercase keys with underscores, like API_TIMEOUT.') }}

-
- - -
-
-
-
-
- -
+ {{ settings_other() }}
{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/sections/translations_section.html b/src/autometabuilder/web/templates/components/sections/translations_section.html index 3d1d35a..e33fd7c 100644 --- a/src/autometabuilder/web/templates/components/sections/translations_section.html +++ b/src/autometabuilder/web/templates/components/sections/translations_section.html @@ -1,6 +1,7 @@ {# Translations section macro. #} {% from "components/atoms/section_header.html" import section_header %} -{% from "components/molecules/empty_state.html" import empty_state %} +{% from "components/organisms/translations_languages.html" import translations_languages with context %} +{% from "components/organisms/translations_editor.html" import translations_editor with context %} {% macro translations_section() -%}
@@ -8,111 +9,11 @@
-
-
-
{{ t('ui.translations.languages', 'Languages') }}
-
-
-
- {% for lang, file in translations.items() %} -
-
- -
- {{ lang.upper() }} - {{ file }} -
-
-
- - {% if lang != 'en' %} - - {% endif %} -
-
- {% endfor %} -
-
- -
+ {{ translations_languages() }}
-
-
-
{{ t('ui.translations.editor.title', 'Translation Editor') }}
- -
-
-
- {{ empty_state("translate", t('ui.translations.empty.title', 'Pick a language'), t('ui.translations.empty.body', 'Select a language from the list to start editing translations.')) }} -
- -
-
+ {{ translations_editor() }}
diff --git a/src/autometabuilder/web/templates/components/sections/workflow_section.html b/src/autometabuilder/web/templates/components/sections/workflow_section.html index 1decc91..b47014c 100644 --- a/src/autometabuilder/web/templates/components/sections/workflow_section.html +++ b/src/autometabuilder/web/templates/components/sections/workflow_section.html @@ -1,10 +1,13 @@ {# Workflow section macro. #} {% from "components/atoms/section_header.html" import section_header %} +{% from "components/organisms/workflow_templates.html" import workflow_templates with context %} {% macro workflow_section() -%}
{{ section_header(t('ui.workflow.title', 'Workflow Builder'), t('ui.workflow.subtitle', "Design the bot's task execution pipeline")) }} + {{ workflow_templates() }} +
{{ t('ui.workflow.card.title', 'Tasks & Steps') }}
diff --git a/src/autometabuilder/workflow_packages/blank.json b/src/autometabuilder/workflow_packages/blank.json new file mode 100644 index 0000000..e665220 --- /dev/null +++ b/src/autometabuilder/workflow_packages/blank.json @@ -0,0 +1,11 @@ +{ + "id": "blank", + "label": "meta.workflow_packages.blank.label", + "description": "meta.workflow_packages.blank.description", + "tags": [ + "starter" + ], + "workflow": { + "nodes": [] + } +} diff --git a/src/autometabuilder/workflow_packages/iterative_loop.json b/src/autometabuilder/workflow_packages/iterative_loop.json new file mode 100644 index 0000000..5ae0a20 --- /dev/null +++ b/src/autometabuilder/workflow_packages/iterative_loop.json @@ -0,0 +1,93 @@ +{ + "id": "iterative_loop", + "label": "meta.workflow_packages.iterative_loop.label", + "description": "meta.workflow_packages.iterative_loop.description", + "tags": [ + "loop", + "tools" + ], + "workflow": { + "nodes": [ + { + "id": "load_context", + "type": "core.load_context", + "outputs": { + "context": "sdlc_context" + } + }, + { + "id": "seed_messages", + "type": "core.seed_messages", + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_context", + "type": "core.append_context_message", + "inputs": { + "messages": "$messages", + "context": "$sdlc_context" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_user_instruction", + "type": "core.append_user_instruction", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "main_loop", + "type": "control.loop", + "inputs": { + "max_iterations": 10, + "stop_when": "$no_tool_calls", + "stop_on": "true" + }, + "body": [ + { + "id": "ai_request", + "type": "core.ai_request", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "response": "llm_response", + "has_tool_calls": "has_tool_calls", + "tool_calls_count": "tool_calls_count" + } + }, + { + "id": "run_tool_calls", + "type": "core.run_tool_calls", + "inputs": { + "response": "$llm_response" + }, + "outputs": { + "tool_results": "tool_results", + "no_tool_calls": "no_tool_calls" + } + }, + { + "id": "append_tool_results", + "type": "core.append_tool_results", + "inputs": { + "messages": "$messages", + "tool_results": "$tool_results" + }, + "outputs": { + "messages": "messages" + } + } + ] + } + ] + } +} diff --git a/src/autometabuilder/workflow_packages/plan_execute_summarize.json b/src/autometabuilder/workflow_packages/plan_execute_summarize.json new file mode 100644 index 0000000..6d642bc --- /dev/null +++ b/src/autometabuilder/workflow_packages/plan_execute_summarize.json @@ -0,0 +1,94 @@ +{ + "id": "plan_execute_summarize", + "label": "meta.workflow_packages.plan_execute_summarize.label", + "description": "meta.workflow_packages.plan_execute_summarize.description", + "tags": [ + "plan", + "summarize" + ], + "workflow": { + "nodes": [ + { + "id": "load_context", + "type": "core.load_context", + "outputs": { + "context": "sdlc_context" + } + }, + { + "id": "seed_messages", + "type": "core.seed_messages", + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_context", + "type": "core.append_context_message", + "inputs": { + "messages": "$messages", + "context": "$sdlc_context" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_user_instruction", + "type": "core.append_user_instruction", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "planner_request", + "type": "core.ai_request", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "response": "llm_response", + "has_tool_calls": "has_tool_calls", + "tool_calls_count": "tool_calls_count" + } + }, + { + "id": "run_tool_calls", + "type": "core.run_tool_calls", + "inputs": { + "response": "$llm_response" + }, + "outputs": { + "tool_results": "tool_results", + "no_tool_calls": "no_tool_calls" + } + }, + { + "id": "append_tool_results", + "type": "core.append_tool_results", + "inputs": { + "messages": "$messages", + "tool_results": "$tool_results" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "summary_request", + "type": "core.ai_request", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "response": "final_response", + "has_tool_calls": "final_has_tool_calls", + "tool_calls_count": "final_tool_calls_count" + } + } + ] + } +} diff --git a/src/autometabuilder/workflow_packages/single_pass.json b/src/autometabuilder/workflow_packages/single_pass.json new file mode 100644 index 0000000..7883596 --- /dev/null +++ b/src/autometabuilder/workflow_packages/single_pass.json @@ -0,0 +1,82 @@ +{ + "id": "single_pass", + "label": "meta.workflow_packages.single_pass.label", + "description": "meta.workflow_packages.single_pass.description", + "tags": [ + "single", + "tools" + ], + "workflow": { + "nodes": [ + { + "id": "load_context", + "type": "core.load_context", + "outputs": { + "context": "sdlc_context" + } + }, + { + "id": "seed_messages", + "type": "core.seed_messages", + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_context", + "type": "core.append_context_message", + "inputs": { + "messages": "$messages", + "context": "$sdlc_context" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "append_user_instruction", + "type": "core.append_user_instruction", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "messages": "messages" + } + }, + { + "id": "ai_request", + "type": "core.ai_request", + "inputs": { + "messages": "$messages" + }, + "outputs": { + "response": "llm_response", + "has_tool_calls": "has_tool_calls", + "tool_calls_count": "tool_calls_count" + } + }, + { + "id": "run_tool_calls", + "type": "core.run_tool_calls", + "inputs": { + "response": "$llm_response" + }, + "outputs": { + "tool_results": "tool_results", + "no_tool_calls": "no_tool_calls" + } + }, + { + "id": "append_tool_results", + "type": "core.append_tool_results", + "inputs": { + "messages": "$messages", + "tool_results": "$tool_results" + }, + "outputs": { + "messages": "messages" + } + } + ] + } +} diff --git a/tests/ui/test_web_ui.py b/tests/ui/test_web_ui.py index 75758a5..87abd93 100644 --- a/tests/ui/test_web_ui.py +++ b/tests/ui/test_web_ui.py @@ -206,9 +206,11 @@ def test_workflow_builder_renders(page: Page, server: str): # Wait for workflow builder to render page.wait_for_selector("#workflow-builder", state="attached") + page.wait_for_selector("#workflow-template-select", state="attached") # Should have at least the primary action button expect(page.locator("#workflow-builder .btn.btn-primary")).to_be_visible() + page.wait_for_function("document.querySelectorAll('#workflow-template-select option').length > 1") # Toggle raw JSON should work page.click(f"#workflow button:has-text('{t('ui.workflow.toggle_json')}')")