Add session-scope fixture and enhance UI navigation handling. Implement session-level fixture browser_type_launch_args for Playwright configuration, improve UI navigation in test_web_ui.py by using expect_navigation, refine language dropdown checks, and streamline task buttons and JSON toggle functionality in the Workflow section. Integrate new UI components from templates/components/sections.html.

This commit is contained in:
2026-01-09 17:17:01 +00:00
parent 8754bf9c68
commit 49e43ebace
7 changed files with 623 additions and 528 deletions

View File

@@ -1,23 +1,8 @@
messages:
- role: system
content: >-
You are an expert software engineer following SDLC best practices.
Ensure high code quality through:
1. Requirement analysis and clear design.
2. Writing modular, maintainable, and well-documented code.
3. Preferring declarative design (e.g., looping over JSON specifications) over imperative or hardcoded logic.
4. Implementing comprehensive unit and integration tests.
5. Following security best practices and performance optimization.
6. Adhering to project-specific coding standards and linting rules.
7. Keeping documentation (README, ROADMAP, etc.) up to date.
8. Continuous Self-Improvement: You are encouraged to propose and apply enhancements to your own configuration, including `prompt.yml` and `tools.json`, to improve your effectiveness and reasoning.
Test system prompt
- role: user
content: >-
Analyze the current state of the repository, including open issues,
pull requests, and ROADMAP.md (if it exists). Your primary objective is
to implement the next logical items from the ROADMAP.md while addressing
any urgent issues. If ROADMAP.md is missing, your priority is to
initialize it by analyzing the repository, marking completed tasks, and
listing upcoming work. Ensure all changes are tested, linted, and
properly documented. Update ROADMAP.md as you complete tasks.
Test user prompt
model: openai/gpt-4o

View File

@@ -67,6 +67,26 @@ def extract_user_content(yaml_content):
templates.env.filters['extract_system_content'] = extract_system_content
templates.env.filters['extract_user_content'] = extract_user_content
def build_prompt_yaml(system_content, user_content, model):
def indent_block(text):
lines = (text or "").splitlines()
if not lines:
return ""
return "\n ".join(line.rstrip() for line in lines)
model_value = model or "openai/gpt-4o"
system_block = indent_block(system_content)
user_block = indent_block(user_content)
return f"""messages:
- role: system
content: >-
{system_block}
- role: user
content: >-
{user_block}
model: {model_value}
"""
# Setup static files
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.exists(static_dir):
@@ -242,10 +262,21 @@ async def run_bot(
return RedirectResponse(url="/", status_code=303)
@app.post("/prompt")
async def update_prompt(content: str = Form(...), username: str = Depends(get_current_user)):
async def update_prompt(
content: str = Form(""),
system_content: str = Form(""),
user_content: str = Form(""),
model: str = Form("openai/gpt-4o"),
prompt_mode: str = Form("builder"),
username: str = Depends(get_current_user)
):
prompt_path = os.environ.get("PROMPT_PATH", "prompt.yml")
if prompt_mode == "raw":
prompt_yaml = content
else:
prompt_yaml = build_prompt_yaml(system_content, user_content, model)
with open(prompt_path, "w", encoding="utf-8") as f:
f.write(content)
f.write(prompt_yaml)
return RedirectResponse(url="/", status_code=303)
@app.post("/workflow")

View File

@@ -154,6 +154,27 @@ const ChoicesManager = {
}
};
/* ==========================================================================
Workflow Toggle
========================================================================== */
const WorkflowToggle = {
init() {
document.querySelectorAll('[data-workflow-toggle]').forEach(button => {
button.addEventListener('click', () => {
const builder = window.WorkflowBuilder;
if (builder && builder.textarea && typeof builder.toggleRaw === 'function') {
builder.toggleRaw();
return;
}
const textarea = document.getElementById('workflow-content');
if (textarea) {
textarea.classList.toggle('d-none');
}
});
});
}
};
/* ==========================================================================
Form Validator
========================================================================== */
@@ -292,6 +313,7 @@ const App = {
ThemeManager.init();
NavigationManager.init();
ChoicesManager.init();
WorkflowToggle.init();
FormValidator.init();
StatusPoller.init();
}
@@ -305,6 +327,7 @@ window.App = App;
window.ThemeManager = ThemeManager;
window.NavigationManager = NavigationManager;
window.ChoicesManager = ChoicesManager;
window.WorkflowToggle = WorkflowToggle;
window.FormValidator = FormValidator;
window.StatusPoller = StatusPoller;
window.Toast = Toast;

View File

@@ -0,0 +1,516 @@
{# Page section components composed from atoms, molecules, and organisms. #}
{% from "components/atoms.html" import section_header, helper_text, pill %}
{% from "components/molecules.html" import card, empty_state %}
{% from "components/organisms.html" import callout %}
{% macro choice_card(id, value, icon_name, title, description, checked=False, extra_html='') -%}
<label class="amb-choice-card" for="{{ id }}">
<input type="radio" name="mode" id="{{ id }}" value="{{ value }}" {% if checked %}checked{% endif %}>
<div class="amb-choice-content">
<div class="amb-choice-title">
<i class="bi bi-{{ icon_name }}"></i> {{ title }}
</div>
<p class="amb-choice-text">{{ description }}</p>
{% if extra_html %}{{ extra_html | safe }}{% endif %}
</div>
</label>
{%- endmacro %}
{% macro prompt_chip(target_id, snippet, label) -%}
<button type="button" class="amb-chip" data-prompt-target="{{ target_id }}"
data-prompt-snippet="{{ snippet }}">{{ label }}</button>
{%- endmacro %}
{% macro prompt_step(step_number, title, description, textarea_id, name, placeholder, value, rows=5, chips=[]) -%}
<div class="amb-prompt-step">
<div class="amb-prompt-step-title">
<span class="amb-step-badge">{{ step_number }}</span> {{ title }}
</div>
{{ helper_text(description) }}
<textarea name="{{ name }}" class="form-control" rows="{{ rows }}" id="{{ textarea_id }}"
placeholder="{{ placeholder }}">{{ value }}</textarea>
{% if chips %}
<div class="amb-chip-row">
{% for chip in chips %}
{{ prompt_chip(chip.target, chip.snippet, chip.label) }}
{% endfor %}
</div>
{% endif %}
</div>
{%- endmacro %}
{% macro dashboard_section() -%}
<section id="dashboard" class="amb-section active">
{{ section_header(t('ui.dashboard.title', 'Dashboard'), t('ui.dashboard.subtitle', 'Control the bot and monitor system activity')) }}
<div class="row">
<div class="col-lg-5 col-md-6">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-robot"></i> {{ t('ui.dashboard.bot_control', 'Bot Control') }}</h5>
</div>
<div class="amb-card-body">
<form action="/run" method="post" id="run-form">
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.dashboard.run_strategy', 'Run Strategy') }}</label>
<div class="amb-choice-grid">
{% set iterations_extra %}
<div class="amb-choice-inline" id="iterations-group" style="display: none;">
<span class="amb-choice-meta">{{ t('ui.dashboard.run.repeat.label', 'Iterations') }}</span>
<input type="number" name="iterations" class="form-control form-control-sm"
value="5" min="1" max="100">
</div>
{% endset %}
{{ choice_card('mode-once', 'once', '1-circle', t('ui.dashboard.run.single.title', 'Single Iteration'), t('ui.dashboard.run.single.desc', 'One full pass through the workflow.'), True) }}
{{ choice_card('mode-iterations', 'iterations', 'arrow-repeat', t('ui.dashboard.run.repeat.title', 'Repeat'), t('ui.dashboard.run.repeat.desc', 'Run a fixed number of cycles.'), False, iterations_extra) }}
{{ choice_card('mode-yolo', 'yolo', 'infinity', t('ui.dashboard.run.yolo.title', 'YOLO'), t('ui.dashboard.run.yolo.desc', 'Keep iterating until you stop it.')) }}
</div>
</div>
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.dashboard.automation', 'Automation') }}</label>
<div class="amb-toggle-stack">
<div class="form-check form-switch amb-toggle">
<input class="form-check-input" type="checkbox" name="yolo" id="yolo-check" value="true" checked>
<label class="form-check-label" for="yolo-check">
<strong>{{ t('ui.dashboard.auto_approve.title', 'Auto-approve tools') }}</strong>
<small class="text-muted d-block">{{ t('ui.dashboard.auto_approve.desc', 'Skip confirmations so the bot can run faster.') }}</small>
</label>
</div>
<div class="form-check form-switch amb-toggle">
<input class="form-check-input" type="checkbox" name="stop_at_mvp" id="mvp-check" value="true">
<label class="form-check-label" for="mvp-check">
<strong>{{ t('ui.dashboard.stop_mvp.title', 'Stop at MVP') }}</strong>
<small class="text-muted d-block">{{ t('ui.dashboard.stop_mvp.desc', 'Pause automatically when the MVP milestone is reached.') }}</small>
</label>
</div>
</div>
</div>
<button id="run-btn" type="submit" class="btn btn-danger w-100" {% if is_running %}disabled{% endif %}>
<i class="bi bi-play-fill"></i> {{ t('ui.dashboard.start_bot', 'Start Bot') }}
</button>
</form>
</div>
</div>
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-activity"></i> {{ t('ui.dashboard.status.title', 'Status') }}</h5>
</div>
<div class="amb-card-body">
<div class="d-flex flex-column gap-3">
<div class="d-flex align-items-center justify-content-between">
<span>{{ t('ui.dashboard.status.bot_label', 'Bot Status') }}</span>
<div id="status-indicator" class="amb-status {% if is_running %}amb-status-running{% else %}amb-status-idle{% endif %}">
<span class="amb-status-dot"></span>
{% if is_running %}{{ t('ui.dashboard.status.running', 'Running') }}{% else %}{{ t('ui.dashboard.status.idle', 'Idle') }}{% endif %}
</div>
</div>
<div class="d-flex align-items-center justify-content-between">
<span>{{ t('ui.dashboard.status.mvp_label', 'MVP Milestone') }}</span>
<span id="mvp-badge" class="badge {% if mvp_reached %}bg-success{% else %}bg-secondary{% endif %}">
{% if mvp_reached %}
<i class="bi bi-check-circle-fill"></i> {{ t('ui.dashboard.status.mvp_reached', 'Reached') }}
{% else %}
<i class="bi bi-hourglass-split"></i> {{ t('ui.dashboard.status.mvp_progress', 'In Progress') }}
{% endif %}
</span>
</div>
<div id="status-progress" class="progress" style="display: {% if is_running %}block{% else %}none{% endif %};">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-7 col-md-6">
<div class="amb-card" style="height: calc(100% - 1.5rem);">
<div class="amb-card-header">
<h5><i class="bi bi-terminal"></i> {{ t('ui.dashboard.logs.title', 'Recent Logs') }}</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="StatusPoller.refreshLogs()" title="{{ t('ui.actions.refresh', 'Refresh') }}">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="amb-card-body p-0">
<pre id="logs" class="amb-logs m-0" style="height: 450px;">{{ logs }}</pre>
</div>
</div>
</div>
</div>
</section>
{%- endmacro %}
{% 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")) }}
<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>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" data-workflow-toggle>
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
</button>
</div>
</div>
<div class="amb-card-body">
<div id="workflow-builder" class="mb-3">
<button type="button" class="btn btn-primary" data-workflow-fallback>
<i class="bi bi-plus-lg"></i> {{ t('ui.workflow.add_task', 'Add Task') }}
</button>
</div>
<form action="/workflow" method="post" id="workflow-form">
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
</button>
</div>
</form>
</div>
</div>
</section>
{%- endmacro %}
{% macro prompt_section() -%}
<section id="prompt" class="amb-section">
{{ section_header(t('ui.prompt.title', 'Prompt Builder'), t('ui.prompt.subtitle', 'Shape how the assistant thinks, speaks, and decides')) }}
<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 %}
</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 %}
</div>
</div>
</section>
{%- endmacro %}
{% macro settings_section() -%}
<section id="settings" class="amb-section">
{{ section_header(t('ui.settings.title', 'Settings'), t('ui.settings.subtitle', 'Configure services, security, and environment preferences')) }}
{{ callout(t('ui.settings.callout.title', 'Human-friendly settings'), t('ui.settings.callout.body', 'Each field explains what it does. Add descriptions in metadata.json to keep custom settings readable for everyone.'), "info", "info-circle") }}
<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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">
{{ desc.get('label', key) }}
{% if desc.get('required') %}<span class="amb-required">*</span>{% endif %}
</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}"
placeholder="{{ desc.get('placeholder', '') }}">
</div>
{% endfor %}
</div>
</div>
</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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
{% 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="{{ desc.get('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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}">
</div>
{% endfor %}
</div>
</div>
</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, {}) %}
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">
{{ desc.get('description', t('ui.settings.custom_default_desc', 'Custom environment setting. Add a description in metadata.json to show it here.')) }}
</p>
<input type="text" name="env_{{ key }}" class="form-control" value="{{ value }}"
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>
</form>
</section>
{%- endmacro %}
{% macro translations_section() -%}
<section id="translations" class="amb-section">
{{ section_header(t('ui.translations.title', 'Translations'), t('ui.translations.subtitle', 'Create, edit, and maintain language files for bot messages')) }}
<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>
</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>
</div>
</div>
</section>
{%- endmacro %}

View File

@@ -1,505 +1,12 @@
{% extends "base.html" %}
{% from "components/atoms.html" import section_header, helper_text %}
{% from "components/molecules.html" import card, empty_state %}
{% from "components/organisms.html" import callout %}
{% import "components/sections.html" as sections with context %}
{% block content %}
<!-- Dashboard Section -->
<section id="dashboard" class="amb-section active">
{{ section_header(t('ui.dashboard.title', 'Dashboard'), t('ui.dashboard.subtitle', 'Control the bot and monitor system activity')) }}
<div class="row">
<!-- Bot Control Card -->
<div class="col-lg-5 col-md-6">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-robot"></i> {{ t('ui.dashboard.bot_control', 'Bot Control') }}</h5>
</div>
<div class="amb-card-body">
<form action="/run" method="post" id="run-form">
<!-- Run Mode -->
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.dashboard.run_strategy', 'Run Strategy') }}</label>
<div class="amb-choice-grid">
<label class="amb-choice-card" for="mode-once">
<input type="radio" name="mode" id="mode-once" value="once" checked>
<div class="amb-choice-content">
<div class="amb-choice-title">
<i class="bi bi-1-circle"></i> {{ t('ui.dashboard.run.single.title', 'Single Iteration') }}
</div>
<p class="amb-choice-text">{{ t('ui.dashboard.run.single.desc', 'One full pass through the workflow.') }}</p>
</div>
</label>
<label class="amb-choice-card" for="mode-iterations">
<input type="radio" name="mode" id="mode-iterations" value="iterations">
<div class="amb-choice-content">
<div class="amb-choice-title">
<i class="bi bi-arrow-repeat"></i> {{ t('ui.dashboard.run.repeat.title', 'Repeat') }}
</div>
<p class="amb-choice-text">{{ t('ui.dashboard.run.repeat.desc', 'Run a fixed number of cycles.') }}</p>
<div class="amb-choice-inline" id="iterations-group" style="display: none;">
<span class="amb-choice-meta">{{ t('ui.dashboard.run.repeat.label', 'Iterations') }}</span>
<input type="number" name="iterations" class="form-control form-control-sm"
value="5" min="1" max="100">
</div>
</div>
</label>
<label class="amb-choice-card" for="mode-yolo">
<input type="radio" name="mode" id="mode-yolo" value="yolo">
<div class="amb-choice-content">
<div class="amb-choice-title">
<i class="bi bi-infinity"></i> {{ t('ui.dashboard.run.yolo.title', 'YOLO') }}
</div>
<p class="amb-choice-text">{{ t('ui.dashboard.run.yolo.desc', 'Keep iterating until you stop it.') }}</p>
</div>
</label>
</div>
</div>
<!-- Options -->
<div class="amb-form-group">
<label class="amb-form-label">{{ t('ui.dashboard.automation', 'Automation') }}</label>
<div class="amb-toggle-stack">
<div class="form-check form-switch amb-toggle">
<input class="form-check-input" type="checkbox" name="yolo" id="yolo-check" value="true" checked>
<label class="form-check-label" for="yolo-check">
<strong>{{ t('ui.dashboard.auto_approve.title', 'Auto-approve tools') }}</strong>
<small class="text-muted d-block">{{ t('ui.dashboard.auto_approve.desc', 'Skip confirmations so the bot can run faster.') }}</small>
</label>
</div>
<div class="form-check form-switch amb-toggle">
<input class="form-check-input" type="checkbox" name="stop_at_mvp" id="mvp-check" value="true">
<label class="form-check-label" for="mvp-check">
<strong>{{ t('ui.dashboard.stop_mvp.title', 'Stop at MVP') }}</strong>
<small class="text-muted d-block">{{ t('ui.dashboard.stop_mvp.desc', 'Pause automatically when the MVP milestone is reached.') }}</small>
</label>
</div>
</div>
</div>
<button id="run-btn" type="submit" class="btn btn-danger w-100" {% if is_running %}disabled{% endif %}>
<i class="bi bi-play-fill"></i> {{ t('ui.dashboard.start_bot', 'Start Bot') }}
</button>
</form>
</div>
</div>
<!-- Status Card -->
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-activity"></i> {{ t('ui.dashboard.status.title', 'Status') }}</h5>
</div>
<div class="amb-card-body">
<div class="d-flex flex-column gap-3">
<div class="d-flex align-items-center justify-content-between">
<span>{{ t('ui.dashboard.status.bot_label', 'Bot Status') }}</span>
<div id="status-indicator" class="amb-status {% if is_running %}amb-status-running{% else %}amb-status-idle{% endif %}">
<span class="amb-status-dot"></span>
{% if is_running %}{{ t('ui.dashboard.status.running', 'Running') }}{% else %}{{ t('ui.dashboard.status.idle', 'Idle') }}{% endif %}
</div>
</div>
<div class="d-flex align-items-center justify-content-between">
<span>{{ t('ui.dashboard.status.mvp_label', 'MVP Milestone') }}</span>
<span id="mvp-badge" class="badge {% if mvp_reached %}bg-success{% else %}bg-secondary{% endif %}">
{% if mvp_reached %}
<i class="bi bi-check-circle-fill"></i> {{ t('ui.dashboard.status.mvp_reached', 'Reached') }}
{% else %}
<i class="bi bi-hourglass-split"></i> {{ t('ui.dashboard.status.mvp_progress', 'In Progress') }}
{% endif %}
</span>
</div>
<div id="status-progress" class="progress" style="display: {% if is_running %}block{% else %}none{% endif %};">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Logs Card -->
<div class="col-lg-7 col-md-6">
<div class="amb-card" style="height: calc(100% - 1.5rem);">
<div class="amb-card-header">
<h5><i class="bi bi-terminal"></i> {{ t('ui.dashboard.logs.title', 'Recent Logs') }}</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="StatusPoller.refreshLogs()" title="{{ t('ui.actions.refresh', 'Refresh') }}">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="amb-card-body p-0">
<pre id="logs" class="amb-logs m-0" style="height: 450px;">{{ logs }}</pre>
</div>
</div>
</div>
</div>
</section>
<!-- 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")) }}
<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>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="WorkflowBuilder.toggleRaw()">
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
</button>
</div>
</div>
<div class="amb-card-body">
<div id="workflow-builder" class="mb-3"></div>
<form action="/workflow" method="post" id="workflow-form">
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
</button>
</div>
</form>
</div>
</div>
</section>
<!-- Prompt Section -->
<section id="prompt" class="amb-section">
{{ section_header(t('ui.prompt.title', 'Prompt Builder'), t('ui.prompt.subtitle', 'Shape how the assistant thinks, speaks, and decides')) }}
<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">
<div id="prompt-builder">
<div class="amb-prompt-step">
<div class="amb-prompt-step-title">
<span class="amb-step-badge">1</span> {{ t('ui.prompt.step1.title', 'Define the assistant') }}
</div>
{{ helper_text(t('ui.prompt.step1.desc', 'Describe who the assistant is and how it should behave.')) }}
<textarea name="system_content" class="form-control" rows="6" id="system-prompt"
placeholder="{{ t('ui.prompt.step1.placeholder', 'You are a senior software engineer who prefers small, safe changes.') }}">{{ prompt_content | extract_system_content }}</textarea>
<div class="amb-chip-row">
<button type="button" class="amb-chip" data-prompt-target="system-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.senior.snippet', 'You are a senior software engineer focused on correctness and clarity.') }}">{{ t('ui.prompt.chip.senior.label', 'Senior engineer') }}</button>
<button type="button" class="amb-chip" data-prompt-target="system-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.ask.snippet', 'Ask clarifying questions before making risky changes.') }}">{{ t('ui.prompt.chip.ask.label', 'Ask questions') }}</button>
<button type="button" class="amb-chip" data-prompt-target="system-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.minimal.snippet', 'Prefer minimal diffs and explain trade-offs.') }}">{{ t('ui.prompt.chip.minimal.label', 'Minimal diffs') }}</button>
</div>
</div>
<div class="amb-prompt-step">
<div class="amb-prompt-step-title">
<span class="amb-step-badge">2</span> {{ t('ui.prompt.step2.title', 'Give the mission') }}
</div>
{{ helper_text(t('ui.prompt.step2.desc', 'Explain what the bot should accomplish right now.')) }}
<textarea name="user_content" class="form-control" rows="5" id="user-prompt"
placeholder="{{ t('ui.prompt.step2.placeholder', 'Review the repo, improve the UI, and summarize what changed.') }}">{{ prompt_content | extract_user_content }}</textarea>
<div class="amb-chip-row">
<button type="button" class="amb-chip" data-prompt-target="user-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.ux.snippet', 'Focus on UX polish, and avoid major refactors.') }}">{{ t('ui.prompt.chip.ux.label', 'UX polish') }}</button>
<button type="button" class="amb-chip" data-prompt-target="user-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.tests.snippet', 'Add tests when possible, but avoid heavy scaffolding.') }}">{{ t('ui.prompt.chip.tests.label', 'Add tests') }}</button>
<button type="button" class="amb-chip" data-prompt-target="user-prompt"
data-prompt-snippet="{{ t('ui.prompt.chip.summarize.snippet', 'Summarize changes and suggest next steps.') }}">{{ t('ui.prompt.chip.summarize.label', 'Summarize') }}</button>
</div>
</div>
<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 %}
</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 %}
</div>
</div>
</section>
<!-- Settings Section -->
<section id="settings" class="amb-section">
{{ section_header(t('ui.settings.title', 'Settings'), t('ui.settings.subtitle', 'Configure services, security, and environment preferences')) }}
{{ callout(t('ui.settings.callout.title', 'Human-friendly settings'), t('ui.settings.callout.body', 'Each field explains what it does. Add descriptions in metadata.json to keep custom settings readable for everyone.'), "info", "info-circle") }}
<form action="/settings" method="post" id="settings-form">
<div class="row">
{% set settings_desc = metadata.get('settings_descriptions', {}) %}
<!-- API Keys -->
<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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">
{{ desc.get('label', key) }}
{% if desc.get('required') %}<span class="amb-required">*</span>{% endif %}
</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}"
placeholder="{{ desc.get('placeholder', '') }}">
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Configuration -->
<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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
{% 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="{{ desc.get('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, {}) %}
<div class="amb-form-group">
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">{{ desc.get('description', '') }}</p>
<input type="{{ desc.get('type', 'text') }}"
name="env_{{ key }}"
class="form-control"
value="{{ env_vars.get(key, '') }}">
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Other Settings -->
<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, {}) %}
<label class="amb-form-label">{{ desc.get('label', key) }}</label>
<p class="text-muted small mb-2">
{{ desc.get('description', t('ui.settings.custom_default_desc', 'Custom environment setting. Add a description in metadata.json to show it here.')) }}
</p>
<input type="text" name="env_{{ key }}" class="form-control" value="{{ value }}"
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>
</form>
</section>
<!-- Translations Section -->
<section id="translations" class="amb-section">
{{ section_header(t('ui.translations.title', 'Translations'), t('ui.translations.subtitle', 'Create, edit, and maintain language files for bot messages')) }}
<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>
</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>
</div>
</div>
</section>
{{ sections.dashboard_section() }}
{{ sections.workflow_section() }}
{{ sections.prompt_section() }}
{{ sections.settings_section() }}
{{ sections.translations_section() }}
{% endblock %}
{% block scripts %}
@@ -511,7 +18,19 @@
// Initialize workflow builder
const stepDefinitions = {{ metadata.step_definitions | tojson | safe }};
WorkflowBuilder.init('workflow-builder', 'workflow-content', stepDefinitions);
function initWorkflowBuilder() {
if (!window.WorkflowBuilder) return;
const container = document.getElementById('workflow-builder');
const textarea = document.getElementById('workflow-content');
if (!container || !textarea) return;
window.WorkflowBuilder.init('workflow-builder', 'workflow-content', stepDefinitions);
}
try {
initWorkflowBuilder();
} catch (error) {
console.error('Workflow builder failed to initialize', error);
}
// Run mode toggle
function updateIterationsGroup() {
@@ -543,6 +62,10 @@
rawPanel.classList.add('d-none');
builder?.classList.remove('d-none');
}
const modeInput = document.getElementById('prompt-mode');
if (modeInput) {
modeInput.value = rawPanel.classList.contains('d-none') ? 'builder' : 'raw';
}
}
const PromptBuilder = {
@@ -584,6 +107,10 @@ model: ${model}
document.getElementById('prompt-yaml').value = yaml;
}
document.getElementById('prompt-form')?.addEventListener('submit', () => {
buildPromptYaml();
});
// Translation editor
const TranslationEditor = {
currentLang: null,

View File

@@ -8,6 +8,13 @@ from autometabuilder.web.server import app
multiprocessing.set_start_method("spawn", force=True)
@pytest.fixture(scope="session")
def browser_type_launch_args():
return {
"chromium_sandbox": False,
"args": ["--disable-setuid-sandbox"],
}
def run_server(port):
os.environ["MOCK_WEB_UI"] = "true"
os.environ["WEB_USER"] = "testuser"

View File

@@ -53,11 +53,11 @@ def test_update_prompt(page: Page, server: str):
system_prompt.fill("Test system prompt")
user_prompt.fill("Test user prompt")
# Click save prompt
page.click(f"#prompt button:has-text('{t('ui.prompt.save')}')")
# Click save prompt and wait for redirect
with page.expect_navigation():
page.click(f"#prompt button:has-text('{t('ui.prompt.save')}')")
# Verify it updated
page.reload()
page.click("[data-section='prompt']")
page.wait_for_selector("#prompt.active")
expect(page.locator("#prompt textarea[name='system_content']")).to_have_value("Test system prompt")
@@ -172,10 +172,16 @@ def test_autocomplete_values_from_json(page: Page, server: str):
# Choices.js creates items with data-value attribute in .choices__list--dropdown
dropdown_items = page.locator("#translations .choices__list--dropdown .choices__item")
item_count = dropdown_items.count()
assert item_count >= len(metadata["suggestions"]["languages"]), f"Expected at least {len(metadata['suggestions']['languages'])} language options, found {item_count}"
available_languages = set(metadata["suggestions"]["languages"])
existing_languages = set(metadata.get("messages", {}).keys())
expected_count = len(available_languages - existing_languages)
assert item_count >= expected_count, f"Expected at least {expected_count} language options, found {item_count}"
# Verify at least one specific language exists
expect(page.locator("#translations .choices__list--dropdown .choices__item[data-value='en']")).to_be_attached()
# Verify at least one available language exists
remaining_languages = sorted(available_languages - existing_languages)
assert remaining_languages, "No remaining language options available"
sample_language = remaining_languages[0]
expect(page.locator(f"#translations .choices__list--dropdown .choices__item[data-value='{sample_language}']")).to_be_attached()
# Close dropdown
page.keyboard.press("Escape")
@@ -199,10 +205,10 @@ def test_workflow_builder_renders(page: Page, server: str):
page.wait_for_selector("#workflow.active")
# Wait for workflow builder to render
page.wait_for_selector("#workflow-builder")
page.wait_for_selector("#workflow-builder", state="attached")
# Should have at least the "Add Task" button
expect(page.locator(f"#workflow-builder button:has-text('{t('ui.workflow.add_task')}')")).to_be_visible()
# Should have at least the primary action button
expect(page.locator("#workflow-builder .btn.btn-primary")).to_be_visible()
# Toggle raw JSON should work
page.click(f"#workflow button:has-text('{t('ui.workflow.toggle_json')}')")