agent-zero/plugins/_email_integration/webui/config.html
Alessandro 2000ba74a3 ui: redesign email, Telegram, and WhatsApp settings
Redesign the three messaging integration panels with a clearer, more guided
setup flow and polished user experience.

- simplify the email panel by surfacing the essentials first, moving
  advanced scheduling behind Advanced, and making connection checks more
  visible
- redesign Telegram and WhatsApp as step-based setup flows with clearer
  status states, safer access warnings, richer test feedback, and more
  responsive layouts
- add shared plugin-settings wizard footer support, extract WhatsApp state
  into its own store, and align test-connection messages with the new UX

ux: ease Email connector setup and refresh copy

- Redesign the Email connector settings around a guided first-run flow with a clearer empty state, provider presets, and much friendlier copy
- Move server, routing, and scheduling power-user controls into an `Advanced` section while keeping the existing config model compatible
- Improve connection-test messaging, add Exchange inbound validation, and refresh the dashboard Email card copy while keeping the card visible
- Verify the updated setup flow in the browser on desktop and mobile

update and simplify x-data based on established frontend patterns

Update 10_discovery_cards.py

further polishing and first-draft no-click model for email and telegram

update whatsapp

Update telegram-config-store.js
2026-04-11 01:40:24 +02:00

778 lines
38 KiB
HTML

<html>
<head>
<title>Email Integration</title>
<script type="module">
import { store } from "/plugins/_email_integration/webui/email-config-store.js";
</script>
</head>
<body>
<div x-data x-init="$store.emailConfig.init(config, context)" x-destroy="$store.emailConfig.cleanup()">
<template x-if="config">
<div class="email-settings">
<div class="section-title">Email</div>
<template x-if="$store.emailConfig.didInit && $store.emailConfig.handlers.length === 0">
<div class="email-empty">
<div class="email-empty-title">Connect Agent Zero and your email account</div>
<div class="email-empty-copy">
Most people only need a provider, an email address, and an app password.
</div>
<button class="btn btn-field" @click="$store.emailConfig.addHandler()">
Connect
</button>
</div>
</template>
<template x-for="(handler, idx) in $store.emailConfig.handlers" :key="idx">
<div class="email-card">
<div class="email-card-header" @click="$store.emailConfig.toggleEditing(idx)">
<div class="email-card-heading">
<div class="email-card-title" x-text="$store.emailConfig.handlerTitle(handler, idx)">
</div>
<div class="email-card-subtitle" x-text="$store.emailConfig.handlerSubtitle(handler)">
</div>
</div>
<div class="email-card-actions">
<span class="email-status-pill"
:class="'tone-' + $store.emailConfig.statusTone(handler)"
x-text="$store.emailConfig.statusLabel(handler)"></span>
<button class="btn btn-action delete"
@click.stop="$confirmClick($event, () => $store.emailConfig.removeHandler(idx))"
title="Remove inbox">
<span class="material-symbols-outlined">delete</span>
</button>
<span class="material-symbols-outlined email-card-chevron"
:class="{ 'is-open': $store.emailConfig.editing === idx }">expand_more</span>
</div>
</div>
<template x-if="$store.emailConfig.editing === idx">
<div class="email-card-body">
<div class="email-intro-copy">
<div x-text="$store.emailConfig.providerHint(handler)"></div>
<template x-if="$store.emailConfig.providerHelpUrl(handler)">
<a class="email-intro-link"
:href="$store.emailConfig.providerHelpUrl(handler)"
target="_blank"
rel="noopener"
x-text="$store.emailConfig.providerHelpLabel(handler)"></a>
</template>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Turn on this inbox</div>
<div class="field-description">Enable when you are ready for Agent Zero to start
checking mail.</div>
</div>
<div class="field-control">
<label class="toggle">
<input type="checkbox" x-model="handler.enabled" />
<span class="toggler"></span>
</label>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Provider</div>
<div class="field-description">Pick the provider first. We will fill in the
common server settings for you.</div>
</div>
<div class="field-control">
<select :value="$store.emailConfig.providerValue(handler)"
@change="$store.emailConfig.applyProvider(handler, $event.target.value)">
<option value="">Choose a provider</option>
<option value="gmail">Gmail</option>
<option value="icloud">iCloud Mail</option>
<option value="microsoft365">Outlook / Microsoft 365</option>
<option value="yahoo">Yahoo Mail</option>
<option value="exchange">Exchange</option>
<option value="custom-imap">Custom IMAP</option>
</select>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Email address</div>
<div class="field-description">Usually the full address for this inbox.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.username"
@input="$store.emailConfig.maybeAutoname(handler)"
placeholder="name@company.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Password</div>
<div class="field-description">An app password is best. Many mail providers
block regular account passwords.</div>
</div>
<div class="field-control">
<input type="password" x-model="handler.password" />
</div>
</div>
<template x-if="$store.emailConfig.showExchangeServer(handler)">
<div>
<div class="field">
<div class="field-label">
<div class="field-title"
x-text="$store.emailConfig.incomingLabel(handler)"></div>
<div class="field-description"
x-text="$store.emailConfig.incomingDescription(handler)"></div>
</div>
<div class="field-control">
<input type="text" x-model="handler.imap_server"
:placeholder="$store.emailConfig.incomingPlaceholder(handler)" />
</div>
</div>
</div>
</template>
<template x-if="$store.emailConfig.showManualServers(handler)">
<div class="email-grid">
<div class="field">
<div class="field-label">
<div class="field-title">Incoming mail server</div>
<div class="field-description">The IMAP server for this inbox.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.imap_server"
placeholder="imap.your-provider.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Incoming port</div>
<div class="field-description">993 is the usual SSL port.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.imap_port"
placeholder="993" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Outgoing mail server</div>
<div class="field-description">The SMTP server used for replies.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.smtp_server"
placeholder="smtp.your-provider.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Outgoing port</div>
<div class="field-description">587 is the usual TLS port.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.smtp_port"
placeholder="587" />
</div>
</div>
</div>
</template>
<template
x-if="!$store.emailConfig.showManualServers(handler) && !$store.emailConfig.showExchangeServer(handler) && $store.emailConfig.providerValue(handler)">
<div class="email-note">
Server settings are filled in for <span
x-text="$store.emailConfig.providerLabel($store.emailConfig.providerValue(handler))"></span>.
You can change them any time in Advanced.
</div>
</template>
<div class="email-missing" x-show="$store.emailConfig.missingBits(handler).length > 0">
Still needed:
<span x-text="$store.emailConfig.missingBits(handler).join(', ')"></span>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Routing instructions</div>
<div class="field-description">Extra guidance for how incoming email should be
routed into chats.</div>
</div>
<div class="field-control">
<textarea x-model="handler.dispatcher_instructions" rows="3"
placeholder="Always start a new chat for invoices or billing questions."></textarea>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Reply instructions</div>
<div class="field-description">Extra guidance for the agent when it replies by
email.</div>
</div>
<div class="field-control">
<textarea x-model="handler.agent_instructions" rows="3"
placeholder="Reply in a calm, concise tone."></textarea>
</div>
</div>
<div class="email-test-panel">
<div class="email-test-copy">
<div class="email-test-title">Check the connection</div>
<div class="email-test-description"
x-text="$store.emailConfig.testIntro(handler)"></div>
</div>
<button class="btn btn-field" @click.stop="$store.emailConfig.testConnection(idx)"
:disabled="$store.emailConfig.testing === idx || !$store.emailConfig.canTest(handler)">
<span x-text="$store.emailConfig.testButtonLabel(handler, idx)"></span>
</button>
</div>
<template
x-if="$store.emailConfig.testResults && $store.emailConfig.testResultsFor === idx">
<div class="email-results"
:class="{ 'is-error': !$store.emailConfig.testResults.success }">
<template x-for="result in $store.emailConfig.testResults.results"
:key="result.test + result.message">
<div class="email-result-row">
<span class="email-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
<div class="email-result-copy">
<div class="email-result-title"
x-text="$store.emailConfig.resultTitle(result)"></div>
<div class="email-result-message"
x-text="$store.emailConfig.resultMessage(result)"></div>
</div>
</div>
</template>
</div>
</template>
<details class="email-advanced">
<summary>
<span>Advanced</span>
<span class="material-symbols-outlined email-advanced-chevron"
aria-hidden="true">keyboard_arrow_down</span>
</summary>
<div class="email-advanced-body">
<div class="field">
<div class="field-label">
<div class="field-title">Inbox name</div>
<div class="field-description">A friendly internal label for this
connection.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.name" placeholder="support" />
</div>
</div>
<div class="email-grid">
<div class="field">
<div class="field-label">
<div class="field-title">Incoming mail server</div>
<div class="field-description">Override the auto-filled incoming
server if needed.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.imap_server"
placeholder="imap.your-provider.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Incoming port</div>
<div class="field-description">993 is the common SSL port.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.imap_port"
placeholder="993" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Outgoing mail server</div>
<div class="field-description">Override the auto-filled reply server
if needed.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.smtp_server"
placeholder="smtp.your-provider.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Outgoing port</div>
<div class="field-description">587 is the common TLS port.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.smtp_port"
placeholder="587" />
</div>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Project</div>
<div class="field-description">Open email conversations inside a
specific project.</div>
</div>
<div class="field-control">
<select :value="handler.project"
@change="handler.project = $event.target.value">
<option value="">No project</option>
<template x-for="proj in $store.emailConfig.projects"
:key="proj.name">
<option :value="proj.name" x-text="proj.title || proj.name"
:selected="handler.project === proj.name"></option>
</template>
</select>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Allowed senders</div>
<div class="field-description">Leave empty to allow anyone. Wildcards
like *@company.com work.</div>
</div>
<div class="field-control">
<input type="text" :value="$store.emailConfig.whitelistText(handler)"
@input="$store.emailConfig.setWhitelist(handler, $event.target.value)"
placeholder="*@company.com, founder@company.com" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Catch up on unread mail</div>
<div class="field-description">On startup, process unread mail from the
last N days. Use 0 for brand-new mail only.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.process_unread_days"
min="0" placeholder="0" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Check for new mail</div>
<div class="field-description"
x-text="$store.emailConfig.frequencyHint(handler)"></div>
</div>
<div class="field-control">
<select :value="$store.emailConfig.frequencyValue(handler)"
@change="$store.emailConfig.applyFrequency(handler, $event.target.value)">
<option value="15">Every 15 seconds</option>
<option value="30">Every 30 seconds</option>
<option value="60">Every minute</option>
<option value="300">Every 5 minutes</option>
<option value="900">Every 15 minutes</option>
<option value="custom">Custom schedule</option>
</select>
</div>
</div>
<div class="field"
x-show="$store.emailConfig.frequencyValue(handler) === 'custom'">
<div class="field-label">
<div class="field-title">Scheduling mode</div>
<div class="field-description">Use seconds for a simple interval, or
switch to cron for full control.</div>
</div>
<div class="field-control">
<select x-model="handler.poll_mode">
<option value="seconds">Seconds</option>
<option value="cron">Cron</option>
</select>
</div>
</div>
<div class="field"
x-show="$store.emailConfig.frequencyValue(handler) === 'custom' && handler.poll_mode === 'seconds'">
<div class="field-label">
<div class="field-title">Poll interval (seconds)</div>
<div class="field-description">The exact delay between inbox checks.
</div>
</div>
<div class="field-control">
<input type="number" x-model.number="handler.poll_interval_seconds"
min="5" placeholder="60" />
</div>
</div>
<div class="field"
x-show="$store.emailConfig.frequencyValue(handler) === 'custom' && handler.poll_mode === 'cron'">
<div class="field-label">
<div class="field-title">Cron expression</div>
<div class="field-description">For example, */2 * * * * checks every two
minutes.</div>
</div>
<div class="field-control">
<input type="text" x-model="handler.poll_interval_cron"
placeholder="*/2 * * * *" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Routing model</div>
<div class="field-description">Utility is faster. Chat is more capable
when routing gets nuanced.</div>
</div>
<div class="field-control">
<select x-model="handler.dispatcher_model">
<option value="utility">Utility</option>
<option value="chat">Chat</option>
</select>
</div>
</div>
</div>
</details>
</div>
</template>
</div>
</template>
<template x-if="$store.emailConfig.handlers.length > 0">
<button class="btn btn-field email-add-another" @click="$store.emailConfig.addHandler()">
Connect another inbox
</button>
</template>
</div>
</template>
</div>
<style>
.email-settings {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.email-advanced summary {
cursor: pointer;
list-style: none;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.75rem;
}
.email-advanced summary::-webkit-details-marker {
display: none;
}
.email-advanced summary {
padding: 0.95rem 1rem;
}
.email-advanced-chevron {
margin-left: auto;
flex: 0 0 auto;
transition: transform 0.18s ease, opacity 0.18s ease;
opacity: 0.72;
}
.email-advanced[open] .email-advanced-chevron {
transform: rotate(180deg);
opacity: 1;
}
.email-guide-card {
border: 1px solid color-mix(in srgb, var(--color-border) 80%, white 20%);
border-radius: 12px;
padding: 0.9rem 1rem;
background: color-mix(in srgb, var(--color-background) 88%, white 12%);
}
.email-guide-title {
font-weight: 700;
margin-bottom: 0.55rem;
}
.email-guide-list {
margin: 0;
padding-left: 1.1rem;
color: var(--color-text-secondary);
line-height: 1.55;
}
.email-empty {
border-radius: 0.5rem;
padding: 1rem;
background: color-mix(in srgb, var(--color-background) 94%, white 6%);
}
.email-empty-title {
font-size: 1.05rem;
font-weight: 700;
}
.email-empty-copy {
margin-top: 0.35rem;
margin-bottom: 0.9rem;
color: var(--color-text-secondary);
line-height: 1.5;
}
.email-card {
border-radius: 0.5rem;
overflow: hidden;
}
.email-card-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
}
.email-card-heading {
min-width: 0;
flex: 1 1 auto;
}
.email-card-title {
font-weight: 700;
font-size: 1rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.email-card-subtitle {
margin-top: 0.3rem;
color: var(--color-text-secondary);
font-size: var(--font-size-small);
line-height: 1.45;
}
.email-card-actions {
display: flex;
align-items: center;
gap: 0.45rem;
flex: 0 0 auto;
}
.email-card-chevron {
transition: transform 0.18s ease;
opacity: 0.7;
}
.email-card-chevron.is-open {
transform: rotate(180deg);
}
.email-intro-copy {
margin: 1rem 0;
padding: 0.85rem 0.95rem;
border-radius: 12px;
background: color-mix(in srgb, #3b82f6 12%, var(--color-background) 88%);
color: var(--color-text-secondary);
line-height: 1.5;
}
.email-intro-link {
display: inline-flex;
margin-top: 0.55rem;
color: var(--color-highlight);
font-weight: 600;
text-decoration: none;
}
.email-intro-link:hover {
text-decoration: underline;
}
.email-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.email-note,
.email-missing {
margin-top: 0.2rem;
margin-bottom: 0.85rem;
padding: 0.8rem 0.9rem;
border-radius: 12px;
font-size: var(--font-size-small);
line-height: 1.5;
}
.email-note {
background: color-mix(in srgb, var(--color-background) 82%, white 18%);
color: var(--color-text-secondary);
}
.email-missing {
background: color-mix(in srgb, #f59e0b 14%, var(--color-background) 86%);
color: var(--color-text-secondary);
}
.email-advanced {
margin-top: 0.95rem;
border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
border-radius: 14px;
overflow: hidden;
}
.email-advanced summary {
padding: 0.9rem 1rem;
background: color-mix(in srgb, var(--color-background) 88%, white 12%);
}
.email-advanced-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.email-test-panel {
margin-top: 1rem;
padding: var(--spacing-xs) 0;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.email-test-copy {
min-width: 0;
}
.email-test-title {
font-weight: 700;
margin-bottom: 0.25rem;
}
.email-test-description {
color: var(--color-text-secondary);
line-height: 1.5;
font-size: var(--font-size-small);
}
.email-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;
}
.email-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%);
}
.email-result-row {
display: flex;
align-items: flex-start;
gap: 0.8rem;
padding: 0.7rem 0;
}
.email-result-row+.email-result-row {
border-top: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
}
.email-result-icon {
width: 1.25rem;
font-weight: 800;
line-height: 1.3;
}
.email-result-copy {
min-width: 0;
}
.email-result-title {
font-weight: 700;
margin-bottom: 0.18rem;
}
.email-result-message {
color: var(--color-text-secondary);
line-height: 1.5;
font-size: var(--font-size-small);
overflow-wrap: anywhere;
}
.email-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;
letter-spacing: 0.01em;
}
.email-status-pill.tone-success {
background: rgba(34, 197, 94, 0.14);
color: #7ee7a4;
}
.email-status-pill.tone-ready {
background: rgba(59, 130, 246, 0.16);
color: #93c5fd;
}
.email-status-pill.tone-warning {
background: rgba(245, 158, 11, 0.16);
color: #fcd34d;
}
.email-status-pill.tone-muted {
background: rgba(148, 163, 184, 0.14);
color: #cbd5e1;
}
.email-add-another {
align-self: flex-start;
}
@media (max-width: 900px) {
.email-guide-body,
.email-grid {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 640px) {
.email-card-header,
.email-test-panel {
flex-direction: column;
align-items: stretch;
}
.email-card-actions {
justify-content: space-between;
width: 100%;
}
.email-status-pill {
min-width: 0;
}
}
</style>
</body>
</html>