mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-17 21:19:46 +00:00
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.
619 lines
26 KiB
HTML
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">⚠</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>
|