Modularize UI with component macros and expand workflow features: Introduce Jinja2 macros for components to enhance maintainability and code reuse. Add new templates and i18n message keys for comprehensive workflow package management. Update workflow.js to include a new function for loading workflows.

This commit is contained in:
2026-01-09 19:25:03 +00:00
parent 8695ca2abf
commit 90c16be8dd
34 changed files with 985 additions and 351 deletions

BIN
.pylint_cache/src_1.stats Normal file

Binary file not shown.

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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."
}

View File

@@ -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",

View File

@@ -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()}

View File

@@ -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();
};

View File

@@ -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');

View File

@@ -20,40 +20,15 @@
{% block head %}{% endblock %}
</head>
<body class="amb-layout">
<!-- Sidebar -->
<aside class="amb-sidebar">
<div class="amb-sidebar-header">
<h5><i class="bi bi-cpu"></i> {{ t('ui.app.name', 'AutoMetabuilder') }}</h5>
</div>
<nav class="amb-nav">
<a href="#dashboard" class="amb-nav-link active" data-section="dashboard">
<i class="bi bi-speedometer2"></i> {{ t('ui.nav.dashboard', 'Dashboard') }}
</a>
<a href="#workflow" class="amb-nav-link" data-section="workflow">
<i class="bi bi-diagram-3"></i> {{ t('ui.nav.workflow', 'Workflow') }}
</a>
<a href="#prompt" class="amb-nav-link" data-section="prompt">
<i class="bi bi-file-text"></i> {{ t('ui.nav.prompt', 'Prompt') }}
</a>
<a href="#settings" class="amb-nav-link" data-section="settings">
<i class="bi bi-gear"></i> {{ t('ui.nav.settings', 'Settings') }}
</a>
<a href="#translations" class="amb-nav-link" data-section="translations">
<i class="bi bi-translate"></i> {{ t('ui.nav.translations', 'Translations') }}
</a>
</nav>
<div class="amb-sidebar-footer">
<div class="amb-user">
<i class="bi bi-person-circle"></i>
<span>{{ username }}</span>
</div>
<button class="amb-theme-toggle" data-theme-toggle title="{{ t('ui.theme_toggle', 'Toggle theme') }}">
<i class="bi bi-sun-fill"></i>
</button>
</div>
</aside>
{% 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')) }}
<!-- Main Content -->
<main class="amb-main">

View File

@@ -0,0 +1,6 @@
{# Sidebar navigation link macro. #}
{% macro nav_link(section, icon_name, label, is_active=False) -%}
<a href="#{{ section }}" class="amb-nav-link{% if is_active %} active{% endif %}" data-section="{{ section }}">
<i class="bi bi-{{ icon_name }}"></i> {{ label }}
</a>
{%- endmacro %}

View File

@@ -0,0 +1,10 @@
{# Sidebar navigation list macro. #}
{% from "components/atoms/nav_link.html" import nav_link %}
{% macro sidebar_nav(items) -%}
<nav class="amb-nav">
{% for item in items %}
{{ nav_link(item.section, item.icon, item.label, item.active) }}
{% endfor %}
</nav>
{%- endmacro %}

View File

@@ -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") %}
<form action="/prompt" method="post" id="prompt-form">
<input type="hidden" name="prompt_mode" id="prompt-mode" value="builder">
<div id="prompt-builder">
{% 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
) }}
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.prompt.model.label', 'Choose a model') }}</label>
{{ helper_text(t('ui.prompt.model.desc', 'Pick the balance of quality and speed that fits the task.')) }}
<select name="model" class="form-select" data-choices>
<option value="openai/gpt-4o" {% if 'gpt-4o' in prompt_content %}selected{% endif %}>GPT-4o ({{ t('ui.prompt.model.recommended', 'Recommended') }})</option>
<option value="openai/gpt-4o-mini" {% if 'gpt-4o-mini' in prompt_content %}selected{% endif %}>GPT-4o Mini ({{ t('ui.prompt.model.faster', 'Faster') }})</option>
<option value="openai/gpt-4-turbo" {% if 'gpt-4-turbo' in prompt_content %}selected{% endif %}>GPT-4 Turbo</option>
<option value="anthropic/claude-3-5-sonnet" {% if 'claude' in prompt_content %}selected{% endif %}>Claude 3.5 Sonnet</option>
</select>
</div>
</div>
<div id="prompt-raw" class="d-none">
<label class="amb-form-label">{{ t('ui.prompt.raw.label', 'Advanced YAML') }}</label>
{{ helper_text(t('ui.prompt.raw.desc', 'Edit the full YAML only if you need fine control.')) }}
<textarea name="content" class="amb-code-editor" id="prompt-yaml" rows="18">{{ prompt_content }}</textarea>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-success" onclick="buildPromptYaml()">
<i class="bi bi-save"></i> {{ t('ui.prompt.save', 'Save Prompt') }}
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleRawPrompt()">
<i class="bi bi-code-slash"></i> {{ t('ui.prompt.advanced_yaml', 'Advanced YAML') }}
</button>
</div>
</form>
{% endcall %}
{%- endmacro %}

View File

@@ -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") %}
<div class="amb-guidance">
<div class="mb-3">
<strong>{{ t('ui.prompt.guidance.keep_human.title', 'Keep it human') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.keep_human.desc', 'Write instructions the way you would brief a teammate.') }}</p>
</div>
<div class="mb-3">
<strong>{{ t('ui.prompt.guidance.be_specific.title', 'Be specific') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.be_specific.desc', 'Mention constraints like time, scope, or testing expectations.') }}</p>
</div>
<div>
<strong>{{ t('ui.prompt.guidance.raw.title', 'Use advanced YAML sparingly') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.raw.desc', 'Only switch to raw YAML if you need full control.') }}</p>
</div>
</div>
{% endcall %}
{%- endmacro %}

View File

@@ -0,0 +1,31 @@
{# Settings API keys card macro. #}
{% macro settings_api_keys() -%}
{% set settings_desc = metadata.get('settings_descriptions', {}) %}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-key"></i> {{ t('ui.settings.api_keys', 'API Keys') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">
{{ t(desc_label, desc_label) }}
{% if desc.get('required') %}<span class="amb-required">*</span>{% endif %}
</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}"
placeholder="{{ t(desc_placeholder, desc_placeholder) }}">
</div>
{% endfor %}
</div>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,37 @@
{# Settings configuration card macro. #}
{% macro settings_configuration() -%}
{% set settings_desc = metadata.get('settings_descriptions', {}) %}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-gear"></i> {{ t('ui.settings.configuration', 'Configuration') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
{% if desc.get('type') == 'select' %}
<select name="env_{{ key }}" class="form-select" data-choices>
<option value="">{{ t('ui.common.select_placeholder', 'Select...') }}</option>
{% for opt in desc.get('options', []) %}
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
</select>
{% else %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, desc.get('default', '')) }}"
placeholder="{{ t(desc_placeholder, desc_placeholder) }}">
{% endif %}
</div>
{% endfor %}
</div>
</div>
{%- endmacro %}

View File

@@ -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'] %}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-sliders"></i> {{ t('ui.settings.other', 'Other Settings') }}</h5>
</div>
<div class="amb-card-body">
<div class="row">
{% for key, value in env_vars.items() %}
{% if key not in known_keys %}
<div class="col-md-6 col-lg-4">
<div class="amb-form-group">
{% 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.')) %}
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
<input type="text" name="env_{{ key }}" class="form-control" value="{{ value }}"
placeholder="{{ t(desc.get('placeholder', ''), desc.get('placeholder', '')) }}">
</div>
</div>
{% endif %}
{% endfor %}
<div class="col-md-6 col-lg-4">
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.settings.add.title', 'Add New Setting') }}</label>
<p class="text-muted small mb-2">{{ t('ui.settings.add.desc', 'Use uppercase keys with underscores, like API_TIMEOUT.') }}</p>
<div class="input-group">
<input type="text" name="new_env_key" class="form-control" placeholder="{{ t('ui.settings.add.placeholder_key', 'KEY') }}">
<input type="text" name="new_env_value" class="form-control" placeholder="{{ t('ui.settings.add.placeholder_value', 'Value') }}">
</div>
</div>
</div>
</div>
</div>
<div class="amb-card-footer">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.settings.save_all', 'Save All Settings') }}
</button>
</div>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,26 @@
{# Settings web access card macro. #}
{% macro settings_web_access() -%}
{% set settings_desc = metadata.get('settings_descriptions', {}) %}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-shield-lock"></i> {{ t('ui.settings.web_access', 'Web UI Access') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}">
</div>
{% endfor %}
</div>
</div>
{%- endmacro %}

View File

@@ -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) -%}
<aside class="amb-sidebar">
{{ sidebar_header(app_name) }}
{{ sidebar_nav(nav_items) }}
{{ sidebar_footer(username, toggle_title) }}
</aside>
{%- endmacro %}

View File

@@ -0,0 +1,12 @@
{# Sidebar footer macro. #}
{% macro sidebar_footer(username, toggle_title) -%}
<div class="amb-sidebar-footer">
<div class="amb-user">
<i class="bi bi-person-circle"></i>
<span>{{ username }}</span>
</div>
<button class="amb-theme-toggle" data-theme-toggle title="{{ toggle_title }}">
<i class="bi bi-sun-fill"></i>
</button>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,6 @@
{# Sidebar header macro. #}
{% macro sidebar_header(app_name) -%}
<div class="amb-sidebar-header">
<h5><i class="bi bi-cpu"></i> {{ app_name }}</h5>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,62 @@
{# Translations editor card macro. #}
{% from "components/molecules/empty_state.html" import empty_state %}
{% macro translations_editor() -%}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-pencil-square"></i> <span id="editor-title">{{ t('ui.translations.editor.title', 'Translation Editor') }}</span></h5>
<div id="editor-actions" class="amb-translation-actions" style="display: none;">
<span id="missing-count" class="amb-pill amb-pill-warning">{{ t('ui.translations.missing_count', '{count} missing') | replace('{count}', '0') }}</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.fillMissing()">
<i class="bi bi-magic"></i> {{ t('ui.translations.actions.fill_missing', 'Fill Missing') }}
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.reset()">
<i class="bi bi-arrow-counterclockwise"></i> {{ t('ui.actions.reset', 'Reset') }}
</button>
<button type="button" class="btn btn-sm btn-success" onclick="TranslationEditor.save()">
<i class="bi bi-save"></i> {{ t('ui.actions.save', 'Save') }}
</button>
</div>
</div>
<div class="amb-card-body">
<div id="translation-editor-placeholder">
{{ 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.')) }}
</div>
<div id="translation-editor" style="display: none;">
<div class="amb-translation-toolbar">
<div class="amb-translation-search">
<input type="search" class="form-control form-control-sm" id="translation-search"
placeholder="{{ t('ui.translations.search.placeholder', 'Search keys or text...') }}" oninput="TranslationEditor.filter(this.value)">
</div>
<div class="form-check form-switch amb-translation-toggle">
<input class="form-check-input" type="checkbox" id="translation-missing-toggle" checked
onchange="TranslationEditor.toggleMissing(this.checked)">
<label class="form-check-label small" for="translation-missing-toggle">{{ t('ui.translations.toggle_missing', 'Show missing keys') }}</label>
</div>
</div>
<div class="amb-translation-add">
<input type="text" class="form-control form-control-sm" id="new-translation-key" placeholder="{{ t('ui.translations.add.key_placeholder', 'new.key') }}">
<input type="text" class="form-control form-control-sm" id="new-translation-value" placeholder="{{ t('ui.translations.add.value_placeholder', 'Translation text') }}">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.prefillNewValue()">
{{ t('ui.translations.add.use_english', 'Use English') }}
</button>
<button type="button" class="btn btn-sm btn-primary" onclick="TranslationEditor.addEntry()">
<i class="bi bi-plus-lg"></i> {{ t('ui.translations.add.add_key', 'Add Key') }}
</button>
</div>
<table class="table table-sm" id="translation-table">
<thead>
<tr>
<th style="width: 30%;">{{ t('ui.translations.table.key', 'Key') }}</th>
<th>{{ t('ui.translations.table.translation', 'Translation') }}</th>
<th style="width: 70px;"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,48 @@
{# Translations languages card macro. #}
{% macro translations_languages() -%}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-globe"></i> {{ t('ui.translations.languages', 'Languages') }}</h5>
</div>
<div class="amb-card-body p-0">
<div class="list-group list-group-flush" id="translation-list">
{% for lang, file in translations.items() %}
<div class="list-group-item d-flex justify-content-between align-items-center translation-item" data-lang="{{ lang }}">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-file-text"></i>
<div>
<strong>{{ lang.upper() }}</strong>
<small class="text-muted d-block">{{ file }}</small>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary" onclick="TranslationEditor.load('{{ lang }}')" title="{{ t('ui.actions.edit', 'Edit') }}">
<i class="bi bi-pencil"></i>
</button>
{% if lang != 'en' %}
<button type="button" class="btn btn-outline-danger" onclick="TranslationEditor.delete('{{ lang }}')" title="{{ t('ui.actions.delete', 'Delete') }}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="amb-card-footer">
<form action="/translations" method="post" class="d-flex gap-2">
<select name="lang" class="form-select form-select-sm" data-choices data-placeholder="{{ t('ui.translations.add_language_placeholder', 'Add language...') }}" required>
<option value="">{{ t('ui.translations.select_language_placeholder', 'Select language...') }}</option>
{% for lang in metadata.suggestions.languages %}
{% if lang not in translations %}
<option value="{{ lang }}">{{ lang }}</option>
{% endif %}
{% endfor %}
</select>
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> {{ t('ui.actions.add', 'Add') }}
</button>
</form>
</div>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,30 @@
{# Workflow templates card macro. #}
{% macro workflow_templates() -%}
<div class="amb-card mb-3">
<div class="amb-card-header">
<h5><i class="bi bi-collection"></i> {{ t('ui.workflow.templates.title', 'Workflow Templates') }}</h5>
</div>
<div class="amb-card-body">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<label class="form-label small text-muted" for="workflow-template-select">
{{ t('ui.workflow.templates.select_label', 'Template') }}
</label>
<select id="workflow-template-select" class="form-select form-select-sm">
<option value="">{{ t('ui.workflow.templates.select_placeholder', 'Select a template...') }}</option>
</select>
</div>
<div class="col-lg-5">
<div id="workflow-template-description" class="small text-muted">
{{ t('ui.workflow.templates.description_placeholder', 'Choose a template to preview what it does.') }}
</div>
</div>
<div class="col-lg-2 text-lg-end">
<button type="button" class="btn btn-outline-primary btn-sm" id="workflow-template-apply" disabled>
<i class="bi bi-box-arrow-in-right"></i> {{ t('ui.workflow.templates.apply', 'Apply Template') }}
</button>
</div>
</div>
</div>
</div>
{%- endmacro %}

View File

@@ -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() -%}
<section id="prompt" class="amb-section">
@@ -10,89 +9,11 @@
<div class="row">
<div class="col-lg-8">
{% call card(t('ui.prompt.card.title', 'Prompt Builder'), "chat-square-text") %}
<form action="/prompt" method="post" id="prompt-form">
<input type="hidden" name="prompt_mode" id="prompt-mode" value="builder">
<div id="prompt-builder">
{% 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
) }}
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.prompt.model.label', 'Choose a model') }}</label>
{{ helper_text(t('ui.prompt.model.desc', 'Pick the balance of quality and speed that fits the task.')) }}
<select name="model" class="form-select" data-choices>
<option value="openai/gpt-4o" {% if 'gpt-4o' in prompt_content %}selected{% endif %}>GPT-4o ({{ t('ui.prompt.model.recommended', 'Recommended') }})</option>
<option value="openai/gpt-4o-mini" {% if 'gpt-4o-mini' in prompt_content %}selected{% endif %}>GPT-4o Mini ({{ t('ui.prompt.model.faster', 'Faster') }})</option>
<option value="openai/gpt-4-turbo" {% if 'gpt-4-turbo' in prompt_content %}selected{% endif %}>GPT-4 Turbo</option>
<option value="anthropic/claude-3-5-sonnet" {% if 'claude' in prompt_content %}selected{% endif %}>Claude 3.5 Sonnet</option>
</select>
</div>
</div>
<div id="prompt-raw" class="d-none">
<label class="amb-form-label">{{ t('ui.prompt.raw.label', 'Advanced YAML') }}</label>
{{ helper_text(t('ui.prompt.raw.desc', 'Edit the full YAML only if you need fine control.')) }}
<textarea name="content" class="amb-code-editor" id="prompt-yaml" rows="18">{{ prompt_content }}</textarea>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-success" onclick="buildPromptYaml()">
<i class="bi bi-save"></i> {{ t('ui.prompt.save', 'Save Prompt') }}
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleRawPrompt()">
<i class="bi bi-code-slash"></i> {{ t('ui.prompt.advanced_yaml', 'Advanced YAML') }}
</button>
</div>
</form>
{% endcall %}
{{ prompt_builder() }}
</div>
<div class="col-lg-4">
{% call card(t('ui.prompt.guidance.title', 'Guidance'), "lightbulb") %}
<div class="amb-guidance">
<div class="mb-3">
<strong>{{ t('ui.prompt.guidance.keep_human.title', 'Keep it human') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.keep_human.desc', 'Write instructions the way you would brief a teammate.') }}</p>
</div>
<div class="mb-3">
<strong>{{ t('ui.prompt.guidance.be_specific.title', 'Be specific') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.be_specific.desc', 'Mention constraints like time, scope, or testing expectations.') }}</p>
</div>
<div>
<strong>{{ t('ui.prompt.guidance.raw.title', 'Use advanced YAML sparingly') }}</strong>
<p class="small text-muted mb-0">{{ t('ui.prompt.guidance.raw.desc', 'Only switch to raw YAML if you need full control.') }}</p>
</div>
</div>
{% endcall %}
{{ prompt_guidance() }}
</div>
</div>
</section>

View File

@@ -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() -%}
<section id="settings" class="amb-section">
@@ -10,138 +14,17 @@
<form action="/settings" method="post" id="settings-form">
<div class="row">
{% set settings_desc = metadata.get('settings_descriptions', {}) %}
<div class="col-lg-6">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-key"></i> {{ t('ui.settings.api_keys', 'API Keys') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">
{{ t(desc_label, desc_label) }}
{% if desc.get('required') %}<span class="amb-required">*</span>{% endif %}
</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}"
placeholder="{{ t(desc_placeholder, desc_placeholder) }}">
</div>
{% endfor %}
</div>
</div>
{{ settings_api_keys() }}
</div>
<div class="col-lg-6">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-gear"></i> {{ t('ui.settings.configuration', 'Configuration') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
{% if desc.get('type') == 'select' %}
<select name="env_{{ key }}" class="form-select" data-choices>
<option value="">{{ t('ui.common.select_placeholder', 'Select...') }}</option>
{% for opt in desc.get('options', []) %}
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
</select>
{% else %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, desc.get('default', '')) }}"
placeholder="{{ t(desc_placeholder, desc_placeholder) }}">
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-shield-lock"></i> {{ t('ui.settings.web_access', 'Web UI Access') }}</h5>
</div>
<div class="amb-card-body">
{% 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', '') %}
<div class="amb-form-group">
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
{% if desc_text %}
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
{% endif %}
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}">
</div>
{% endfor %}
</div>
</div>
{{ settings_configuration() }}
{{ settings_web_access() }}
</div>
</div>
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-sliders"></i> {{ t('ui.settings.other', 'Other Settings') }}</h5>
</div>
<div class="amb-card-body">
<div class="row">
{% 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 %}
<div class="col-md-6 col-lg-4">
<div class="amb-form-group">
{% 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.')) %}
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
<p class="text-muted small mb-2">{{ t(desc_text, desc_text) }}</p>
<input type="text" name="env_{{ key }}" class="form-control" value="{{ value }}"
placeholder="{{ t(desc.get('placeholder', ''), desc.get('placeholder', '')) }}">
</div>
</div>
{% endif %}
{% endfor %}
<div class="col-md-6 col-lg-4">
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.settings.add.title', 'Add New Setting') }}</label>
<p class="text-muted small mb-2">{{ t('ui.settings.add.desc', 'Use uppercase keys with underscores, like API_TIMEOUT.') }}</p>
<div class="input-group">
<input type="text" name="new_env_key" class="form-control" placeholder="{{ t('ui.settings.add.placeholder_key', 'KEY') }}">
<input type="text" name="new_env_value" class="form-control" placeholder="{{ t('ui.settings.add.placeholder_value', 'Value') }}">
</div>
</div>
</div>
</div>
</div>
<div class="amb-card-footer">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.settings.save_all', 'Save All Settings') }}
</button>
</div>
</div>
{{ settings_other() }}
</form>
</section>
{%- endmacro %}

View File

@@ -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() -%}
<section id="translations" class="amb-section">
@@ -8,111 +9,11 @@
<div class="row">
<div class="col-lg-4">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-globe"></i> {{ t('ui.translations.languages', 'Languages') }}</h5>
</div>
<div class="amb-card-body p-0">
<div class="list-group list-group-flush" id="translation-list">
{% for lang, file in translations.items() %}
<div class="list-group-item d-flex justify-content-between align-items-center translation-item" data-lang="{{ lang }}">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-file-text"></i>
<div>
<strong>{{ lang.upper() }}</strong>
<small class="text-muted d-block">{{ file }}</small>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary" onclick="TranslationEditor.load('{{ lang }}')" title="{{ t('ui.actions.edit', 'Edit') }}">
<i class="bi bi-pencil"></i>
</button>
{% if lang != 'en' %}
<button type="button" class="btn btn-outline-danger" onclick="TranslationEditor.delete('{{ lang }}')" title="{{ t('ui.actions.delete', 'Delete') }}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="amb-card-footer">
<form action="/translations" method="post" class="d-flex gap-2">
<select name="lang" class="form-select form-select-sm" data-choices data-placeholder="{{ t('ui.translations.add_language_placeholder', 'Add language...') }}" required>
<option value="">{{ t('ui.translations.select_language_placeholder', 'Select language...') }}</option>
{% for lang in metadata.suggestions.languages %}
{% if lang not in translations %}
<option value="{{ lang }}">{{ lang }}</option>
{% endif %}
{% endfor %}
</select>
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> {{ t('ui.actions.add', 'Add') }}
</button>
</form>
</div>
</div>
{{ translations_languages() }}
</div>
<div class="col-lg-8">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-pencil-square"></i> <span id="editor-title">{{ t('ui.translations.editor.title', 'Translation Editor') }}</span></h5>
<div id="editor-actions" class="amb-translation-actions" style="display: none;">
<span id="missing-count" class="amb-pill amb-pill-warning">{{ t('ui.translations.missing_count', '{count} missing') | replace('{count}', '0') }}</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.fillMissing()">
<i class="bi bi-magic"></i> {{ t('ui.translations.actions.fill_missing', 'Fill Missing') }}
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.reset()">
<i class="bi bi-arrow-counterclockwise"></i> {{ t('ui.actions.reset', 'Reset') }}
</button>
<button type="button" class="btn btn-sm btn-success" onclick="TranslationEditor.save()">
<i class="bi bi-save"></i> {{ t('ui.actions.save', 'Save') }}
</button>
</div>
</div>
<div class="amb-card-body">
<div id="translation-editor-placeholder">
{{ 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.')) }}
</div>
<div id="translation-editor" style="display: none;">
<div class="amb-translation-toolbar">
<div class="amb-translation-search">
<input type="search" class="form-control form-control-sm" id="translation-search"
placeholder="{{ t('ui.translations.search.placeholder', 'Search keys or text...') }}" oninput="TranslationEditor.filter(this.value)">
</div>
<div class="form-check form-switch amb-translation-toggle">
<input class="form-check-input" type="checkbox" id="translation-missing-toggle" checked
onchange="TranslationEditor.toggleMissing(this.checked)">
<label class="form-check-label small" for="translation-missing-toggle">{{ t('ui.translations.toggle_missing', 'Show missing keys') }}</label>
</div>
</div>
<div class="amb-translation-add">
<input type="text" class="form-control form-control-sm" id="new-translation-key" placeholder="{{ t('ui.translations.add.key_placeholder', 'new.key') }}">
<input type="text" class="form-control form-control-sm" id="new-translation-value" placeholder="{{ t('ui.translations.add.value_placeholder', 'Translation text') }}">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="TranslationEditor.prefillNewValue()">
{{ t('ui.translations.add.use_english', 'Use English') }}
</button>
<button type="button" class="btn btn-sm btn-primary" onclick="TranslationEditor.addEntry()">
<i class="bi bi-plus-lg"></i> {{ t('ui.translations.add.add_key', 'Add Key') }}
</button>
</div>
<table class="table table-sm" id="translation-table">
<thead>
<tr>
<th style="width: 30%;">{{ t('ui.translations.table.key', 'Key') }}</th>
<th>{{ t('ui.translations.table.translation', 'Translation') }}</th>
<th style="width: 70px;"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
{{ translations_editor() }}
</div>
</div>
</section>

View File

@@ -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 id="workflow" class="amb-section">
{{ section_header(t('ui.workflow.title', 'Workflow Builder'), t('ui.workflow.subtitle', "Design the bot's task execution pipeline")) }}
{{ workflow_templates() }}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-diagram-3"></i> {{ t('ui.workflow.card.title', 'Tasks & Steps') }}</h5>

View File

@@ -0,0 +1,11 @@
{
"id": "blank",
"label": "meta.workflow_packages.blank.label",
"description": "meta.workflow_packages.blank.description",
"tags": [
"starter"
],
"workflow": {
"nodes": []
}
}

View File

@@ -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"
}
}
]
}
]
}
}

View File

@@ -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"
}
}
]
}
}

View File

@@ -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"
}
}
]
}
}

View File

@@ -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')}')")