agent-zero/plugins/_whatsapp_integration/webui/config.html
Alessandro e2c5f59754 fix integration config UI persistence for Telegram + WhatsApp
Align integration config UX/saving behavior and project binding.

- Telegram integration: keep Save available when appropriate in the wizard footer
  so config changes (including deleting bots) can be persisted reliably.
- WhatsApp integration: fix project dropdown binding by using normalized
  key/value helpers (supports both `key/label` and `name/title` payloads) and
  showing project labels correctly.
- Mirror both fixes into the runtime plugin copy under agentdocker so the
  running container gets the update immediately.

This resolves user-reported integration setup issues and restores expected
save/delete behavior.
2026-04-21 06:01:36 +02:00

619 lines
26 KiB
HTML

<html>
<head>
<title>WhatsApp Integration</title>
<script type="module">
import { store } from "/plugins/_whatsapp_integration/webui/whatsapp-config-store.js";
</script>
</head>
<body>
<div x-data x-init="$store.whatsappConfig.init(config, context)" x-destroy="$store.whatsappConfig.cleanup()">
<template x-if="config">
<div class="wa-page">
<div class="section-title">WhatsApp</div>
<template x-if="config.enabled && $store.whatsappConfig.hasMeaningfulConfig()">
<div class="wa-summary-card">
<div class="wa-summary-copy">
<div class="wa-summary-title">Current setup</div>
<div class="wa-summary-subtitle"
x-text="$store.whatsappConfig.modeSummary() + ' · ' + $store.whatsappConfig.projectSummary()">
</div>
</div>
<span class="wa-status-pill" :class="'tone-' + $store.whatsappConfig.statusTone()"
x-text="$store.whatsappConfig.statusLabel()"></span>
</div>
</template>
<div class="wa-wizard-card">
<div class="wa-step-header">
<div class="wa-step-copy">
<div class="wa-step-title" x-text="$store.whatsappConfig.currentStepMeta().title"></div>
<div class="wa-step-description"
x-text="$store.whatsappConfig.currentStepMeta().description"></div>
</div>
<div class="wa-step-dots" aria-hidden="true">
<template x-for="(step, idx) in $store.whatsappConfig.steps" :key="step.title">
<button type="button" class="wa-step-dot"
:class="{ 'is-active': $store.whatsappConfig.currentStep === idx }"
@click="$store.whatsappConfig.setStep(idx)"></button>
</template>
</div>
</div>
<template x-if="$store.whatsappConfig.currentStep === 0">
<div class="wa-step-panel">
<div class="field">
<div class="field-label">
<div class="field-title">Turn on WhatsApp</div>
<div class="field-description">Enable the local bridge to start.</div>
</div>
<div class="field-control">
<label class="toggle">
<input type="checkbox"
x-model="config.enabled"
@change="$store.whatsappConfig.onEnabledChange()" />
<span class="toggler"></span>
</label>
</div>
</div>
<template x-if="config.enabled">
<div>
<div class="field">
<div class="field-label">
<div class="field-title">WhatsApp account</div>
<div class="field-description">
<span x-show="!$store.whatsappConfig.disconnectMessage">Click "Show QR code" to pair your WhatsApp account.</span>
<span x-show="$store.whatsappConfig.disconnectMessage"
x-text="$store.whatsappConfig.disconnectMessage"></span>
</div>
</div>
<div class="field-control wa-inline-actions">
<button class="btn btn-field" @click="$store.whatsappConfig.showQr()">
Show QR code
</button>
<button class="btn btn-field" @click="$store.whatsappConfig.disconnectAccount()"
:disabled="$store.whatsappConfig.disconnecting">
<span x-show="!$store.whatsappConfig.disconnecting">Disconnect</span>
<span x-show="$store.whatsappConfig.disconnecting">Disconnecting...</span>
</button>
</div>
</div>
<template x-if="$store.whatsappConfig.qrVisible">
<div class="wa-qr-panel">
<template x-if="$store.whatsappConfig.qrStatus === 'connected'">
<div>
<div class="wa-qr-status ok">Connected</div>
<div class="wa-qr-message" x-text="$store.whatsappConfig.qrMessage"></div>
<button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
style="margin-top: 12px;">
Close
</button>
</div>
</template>
<template
x-if="$store.whatsappConfig.qrStatus === 'waiting_scan' && $store.whatsappConfig.qrDataUrl">
<div>
<div class="wa-qr-status">Scan with WhatsApp on your phone</div>
<img :src="$store.whatsappConfig.qrDataUrl" alt="WhatsApp QR Code"
class="wa-qr-image" />
<div class="wa-qr-help">The QR code refreshes automatically.</div>
<button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
style="margin-top: 12px;">
Cancel
</button>
</div>
</template>
<template
x-if="$store.whatsappConfig.qrStatus !== 'connected' && !($store.whatsappConfig.qrStatus === 'waiting_scan' && $store.whatsappConfig.qrDataUrl)">
<div>
<div class="wa-qr-status"
x-text="$store.whatsappConfig.qrMessage || 'Connecting...'"></div>
<div class="wa-qr-help"
x-show="$store.whatsappConfig.qrStatus !== 'error'">
Please wait...</div>
<div class="wa-qr-help error"
x-show="$store.whatsappConfig.qrStatus === 'error'"
x-text="$store.whatsappConfig.qrMessage"></div>
<button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
style="margin-top: 12px;">
Cancel
</button>
</div>
</template>
</div>
</template>
</div>
</template>
<div class="wa-note" x-show="!config.enabled">
Turn on WhatsApp to pair or switch your account.
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Mode</div>
<div class="field-description">
<span x-show="config.mode === 'self-chat'">
Use your own number. You can message yourself to talk to the agent.
</span>
<span x-show="config.mode !== 'self-chat'">
Use a separate number dedicated to Agent Zero conversations.
</span>
</div>
</div>
<div class="field-control">
<select x-model="config.mode">
<option value="self-chat">Personal number (self-chat)</option>
<option value="dedicated">Separate number (dedicated)</option>
</select>
</div>
</div>
<div class="wa-mode-note">
<strong>Good to know:</strong> Self-chat uses your own number. Dedicated is better for
shared or public access. You can pair now or come back to it later.
</div>
<div class="wa-warning" x-show="$store.whatsappConfig.accessWarning()">
<div class="wa-warning-title">
<span aria-hidden="true">&#9888;</span>
<span>Warning</span>
</div>
<div class="wa-warning-body" x-text="$store.whatsappConfig.accessWarning()"></div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Allowed numbers</div>
<div class="field-description">Comma-separated phone numbers. Punctuation and +
prefixes are okay. Leave empty only if you want open access.</div>
</div>
<div class="field-control">
<input type="text" :value="$store.whatsappConfig.allowedText()"
@input="$store.whatsappConfig.setAllowed($event.target.value)"
placeholder="+1 (415) 555-1234, +44 7911 123456" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Allow groups</div>
<div class="field-description">Reply in group chats when mentioned or replied to.
</div>
</div>
<div class="field-control">
<label class="toggle">
<input type="checkbox" x-model="config.allow_group" />
<span class="toggler"></span>
</label>
</div>
</div>
</div>
</template>
<template x-if="$store.whatsappConfig.currentStep === 1">
<div class="wa-step-panel">
<div class="field">
<div class="field-label">
<div class="field-title">Project</div>
<div class="field-description">Optional project to activate for WhatsApp
conversations.</div>
</div>
<div class="field-control">
<select :value="config.project" @change="config.project = $event.target.value">
<option value="">No project</option>
<template x-for="proj in $store.whatsappConfig.projects" :key="$store.whatsappConfig.projectOptionValue(proj)">
<option :value="$store.whatsappConfig.projectOptionValue(proj)"
x-text="$store.whatsappConfig.projectOptionLabel(proj)"
:selected="config.project === $store.whatsappConfig.projectOptionValue(proj)">
</option>
</template>
</select>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Agent instructions</div>
<div class="field-description">Extra guidance for how the agent should reply in
WhatsApp chats.</div>
</div>
<div class="field-control">
<textarea x-model="config.agent_instructions" rows="3"
placeholder="Reply briefly and naturally, like a mobile conversation."></textarea>
</div>
</div>
</div>
</template>
<div class="wa-test-panel">
<div class="wa-test-copy">
<div class="wa-test-title">Check the connection</div>
<div class="wa-test-description">We will check whether the local WhatsApp bridge is up and
connected.</div>
</div>
<button class="btn btn-field" @click="$store.whatsappConfig.testConnection()"
:disabled="$store.whatsappConfig.testing">
<span x-text="$store.whatsappConfig.testButtonLabel()"></span>
</button>
</div>
<template x-if="$store.whatsappConfig.testResults">
<div class="wa-results" :class="{ 'is-error': !$store.whatsappConfig.testResults.success }">
<template x-for="result in $store.whatsappConfig.testResults.results"
:key="result.test + result.message">
<div class="wa-result-row">
<span class="wa-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
<div class="wa-result-copy">
<div class="wa-result-title" x-text="result.test"></div>
<div class="wa-result-message" x-text="result.message"></div>
</div>
</div>
</template>
</div>
</template>
<template x-if="$store.whatsappConfig.currentStep > 0">
<details class="wa-advanced">
<summary>
<span>Advanced</span>
<span class="material-symbols-outlined wa-advanced-chevron"
aria-hidden="true">keyboard_arrow_down</span>
</summary>
<div class="wa-advanced-body">
<div class="wa-info-box">
These settings are here when you need them, but the defaults are fine for most
setups.
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Bridge port</div>
<div class="field-description">Local port for the WhatsApp bridge HTTP server.
</div>
</div>
<div class="field-control">
<input type="number" x-model.number="config.bridge_port" placeholder="3100" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Poll interval (seconds)</div>
<div class="field-description">How often to check for new messages. The minimum
is 2 seconds.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="config.poll_interval_seconds" min="2"
placeholder="3" />
</div>
</div>
</div>
</details>
</template>
</div>
</div>
</template>
</div>
<style>
.wa-page {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.wa-summary-card,
.wa-wizard-card {
border-radius: 0.5rem;
}
.wa-advanced summary {
cursor: pointer;
list-style: none;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.75rem;
}
.wa-advanced summary::-webkit-details-marker {
display: none;
}
.wa-summary-title,
.wa-step-title,
.wa-test-title {
font-weight: 700;
}
.wa-summary-subtitle,
.wa-step-description,
.wa-note,
.wa-mode-note,
.wa-test-description,
.wa-result-message {
color: var(--color-text-secondary);
line-height: 1.5;
}
.wa-summary-card {
padding: 1rem;
border: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.wa-step-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1rem;
}
.wa-step-dots {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
.wa-step-dot {
width: 0.85rem;
height: 0.85rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background: transparent;
cursor: pointer;
padding: 0;
}
.wa-step-dot.is-active {
background: rgba(59, 130, 246, 0.9);
border-color: rgba(59, 130, 246, 0.9);
}
.wa-step-panel {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.wa-info-box,
.wa-note {
margin-bottom: 0.9rem;
padding: 0.8rem 0.9rem;
border-radius: 12px;
font-size: var(--font-size-small);
}
.wa-info-box {
background: color-mix(in srgb, #3b82f6 12%, var(--color-background) 88%);
}
.wa-note {
background: color-mix(in srgb, var(--color-background) 88%, white 12%);
}
.wa-warning {
margin: 0.5rem 0 1.25rem;
padding: 0.75rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255, 170, 0, 0.45);
background: rgba(255, 170, 0, 0.12);
color: var(--color-warning-text);
font-size: var(--font-size-small);
}
.wa-warning-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.wa-warning-body {
margin-top: 0.25rem;
line-height: 1.45;
}
.wa-mode-note {
margin-top: -0.2rem;
margin-bottom: 0.9rem;
font-size: var(--font-size-small);
}
.wa-inline-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.wa-qr-panel {
margin-top: 0.25rem;
padding: 1rem;
border-radius: 14px;
border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
background: color-mix(in srgb, var(--color-background) 90%, white 10%);
text-align: center;
}
.wa-qr-status {
font-weight: 700;
margin-bottom: 0.5rem;
}
.wa-qr-status.ok {
color: #7ee7a4;
}
.wa-qr-message,
.wa-qr-help {
color: var(--color-text-secondary);
line-height: 1.5;
}
.wa-qr-help {
font-size: var(--font-size-small);
margin-top: 0.4rem;
}
.wa-qr-help.error {
color: #fca5a5;
}
.wa-qr-image {
width: 256px;
height: 256px;
max-width: 100%;
border-radius: 8px;
background: white;
padding: 4px;
}
.wa-advanced {
margin-top: 1rem;
border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
border-radius: 14px;
overflow: hidden;
}
.wa-advanced summary {
padding: 0.9rem 1rem;
background: color-mix(in srgb, var(--color-background) 88%, white 12%);
}
.wa-advanced-chevron {
margin-left: auto;
flex: 0 0 auto;
transition: transform 0.18s ease, opacity 0.18s ease;
opacity: 0.72;
}
.wa-advanced[open] .wa-advanced-chevron {
transform: rotate(180deg);
opacity: 1;
}
.wa-advanced-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.wa-test-panel {
margin-top: 1rem;
padding: var(--spacing-xs) 0;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.wa-test-copy,
.wa-result-copy {
min-width: 0;
}
.wa-results {
margin-top: 0.9rem;
border-radius: 14px;
border: 1px solid color-mix(in srgb, #22c55e 28%, var(--color-border) 72%);
background: color-mix(in srgb, #22c55e 8%, var(--color-background) 92%);
padding: 0.35rem 0.9rem;
}
.wa-results.is-error {
border-color: color-mix(in srgb, #ef4444 28%, var(--color-border) 72%);
background: color-mix(in srgb, #ef4444 8%, var(--color-background) 92%);
}
.wa-result-row {
display: flex;
align-items: flex-start;
gap: 0.8rem;
padding: 0.7rem 0;
}
.wa-result-row+.wa-result-row {
border-top: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
}
.wa-result-icon {
width: 1.25rem;
font-weight: 800;
line-height: 1.3;
}
.wa-result-title {
font-weight: 700;
margin-bottom: 0.18rem;
}
.wa-status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 5.25rem;
padding: 0.35rem 0.7rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
}
.wa-status-pill.tone-success {
background: rgba(34, 197, 94, 0.14);
color: #7ee7a4;
}
.wa-status-pill.tone-ready {
background: rgba(59, 130, 246, 0.16);
color: #93c5fd;
}
.wa-status-pill.tone-warning {
background: rgba(245, 158, 11, 0.16);
color: #fcd34d;
}
.wa-status-pill.tone-muted {
background: rgba(148, 163, 184, 0.14);
color: #cbd5e1;
}
@media (max-width: 640px) {
.wa-summary-card,
.wa-step-header,
.wa-test-panel {
flex-direction: column;
align-items: stretch;
}
.wa-step-dots {
width: 100%;
justify-content: space-between;
}
.wa-status-pill {
min-width: 0;
align-self: flex-start;
}
}
</style>
</body>
</html>