agent-zero/plugins/_onboarding/webui/onboarding.html
Alessandro 061b43298b Polish onboarding and email setup UX
Add a clearer onboarding progression for Main Model, Utility Model, and ready states with streamlined copy and calmer calls to action.

Rework email integration setup around provider-first selection, guided defaults, and cleaner account fields.
2026-04-27 03:00:09 +02:00

501 lines
29 KiB
HTML

<html class="onboarding-modal">
<head>
<title>Welcome to Agent Zero</title>
<script type="module">
import { store } from "/plugins/_onboarding/webui/onboarding-store.js";
</script>
<style>
.modal-inner.onboarding-modal {
width: min(92vw, 980px);
}
.modal-inner.onboarding-modal .modal-scroll {
padding: 0;
}
.modal-inner.onboarding-modal .modal-bd.onboarding-body {
padding: 24px 32px 28px;
}
.onboarding-logo {
text-align: center;
margin-bottom: 18px;
}
.onboarding-logo img {
width: 200px;
max-width: 100%;
height: auto;
}
.onboarding-welcome-text {
text-align: center;
max-width: 78ch;
margin: var(--spacing-md) auto var(--spacing-lg);
color: var(--text-2);
font-size: 1.02rem;
line-height: 1.5;
}
.onboarding-welcome-title {
color: var(--text-1);
font-size: 1.42rem;
font-weight: 800;
margin-bottom: 12px;
}
.onboarding-progress {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
margin: 0 0 24px;
padding: 7px;
border: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-panel) 42%, transparent);
}
.onboarding-progress-step {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
min-height: 36px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 7px;
color: var(--color-text);
opacity: 0.64;
font-size: 0.84rem;
font-weight: 700;
}
.onboarding-progress-step.active {
opacity: 1;
border-color: color-mix(in srgb, var(--color-primary) 32%, var(--color-border));
background: color-mix(in srgb, var(--color-background-hover) 58%, transparent);
}
.onboarding-progress-step.done {
opacity: 0.88;
}
.onboarding-progress-dot {
display: grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 999px;
background: color-mix(in srgb, var(--color-border) 70%, transparent);
color: var(--color-background);
font-size: 0.72rem;
font-weight: 900;
flex: 0 0 auto;
}
.onboarding-progress-step.active .onboarding-progress-dot {
background: var(--color-primary);
}
.onboarding-progress-step.done .onboarding-progress-dot {
background: #63c98b;
}
.onboarding-progress-check {
font-size: 14px;
line-height: 1;
}
.onboarding-progress-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.onboarding-success {
text-align: center;
padding: var(--spacing-md) 0;
}
.onboarding-success-icon {
font-size: 64px;
color: var(--success, #22c55e);
margin-bottom: 24px;
}
.onboarding-success-text {
margin-bottom: 0;
}
.onboarding-advanced-link {
text-align: center;
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--surface-3);
}
.onboarding-advanced-link a {
color: var(--text-3);
text-decoration: none;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 4px;
}
.onboarding-advanced-link a:hover {
color: var(--text-1);
}
.onboarding-advanced-link-icon {
font-size: 16px;
}
/* Scoped overrides to make the fields look nice here */
.onboarding-body .model-section {
padding: 16px;
background: color-mix(in srgb, var(--color-panel) 58%, transparent);
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
}
.onboarding-body .section-title { margin-bottom: 8px; }
.onboarding-body .section-description { margin-bottom: 24px; }
.onboarding-body .model-section .field:last-child {
margin-bottom: 0;
}
.onboarding-body .loading-container { height: 200px; }
.onboarding-body .input-with-icon { padding-right: 32px; }
.onboarding-body .relative-container { position: relative; }
.onboarding-footer-left { flex: 1; display: flex; gap: 8px; }
.onboarding-footer-right { display: flex; gap: 8px; }
.onboarding-icon-right { font-size: 18px; margin-left: 4px; }
.onboarding-banner-btn-container { margin-top: 12px; }
/* Same as plugins/_model_config/webui/config.html: icons sit inside padded inputs */
.onboarding-body .eye-toggle {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
cursor: pointer;
user-select: none;
opacity: 0.6;
z-index: 1;
}
.onboarding-body .eye-toggle:hover {
opacity: 1;
}
.onboarding-body .model-search-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
display: grid;
place-items: center;
cursor: pointer;
user-select: none;
opacity: 0.6;
z-index: 1;
}
.onboarding-body .model-search-btn:hover {
opacity: 1;
}
.onboarding-body .model-search-btn > span {
grid-area: 1 / 1;
font-size: 18px;
transition: opacity 0.15s;
}
.onboarding-body .model-search-spinner {
animation: onboarding-model-search-spin 0.8s linear infinite;
}
@keyframes onboarding-model-search-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.onboarding-body .model-search-results {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: var(--color-input);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 50;
padding: 4px;
}
.onboarding-body .model-search-item {
padding: 5px 8px;
font-size: 0.8rem;
border-radius: 4px;
cursor: pointer;
word-break: break-all;
}
.onboarding-body .model-search-item:hover {
background: var(--color-background-hover, rgba(255,255,255,0.06));
}
.onboarding-body .model-search-item.disabled {
opacity: 0.4;
cursor: default;
font-style: italic;
}
.onboarding-body .model-search-item.matched {
font-weight: 500;
}
.onboarding-body .model-search-separator {
height: 1px;
margin: 4px 8px;
background: var(--color-border);
opacity: 0.5;
}
.onboarding-body .model-search-item.disabled:hover {
background: transparent;
}
@media (max-width: 720px) {
.modal-inner.onboarding-modal .modal-bd.onboarding-body {
padding: 18px 16px 22px;
}
.onboarding-progress {
grid-template-columns: 1fr;
}
.onboarding-welcome-text {
font-size: 0.96rem;
}
}
</style>
</head>
<body>
<div x-data>
<template x-if="$store.onboarding">
<div x-init="$store.onboarding.onOpen()" x-destroy="$store.onboarding.cleanup()">
<div class="modal-header">
<div class="onboarding-logo">
<img src="/public/a0-fullDark.svg" alt="Agent Zero">
</div>
</div>
<div class="modal-scroll">
<div class="modal-bd onboarding-body">
<div x-show="$store.onboarding.loading" class="loading loading-container"></div>
<template x-if="!$store.onboarding.loading && $store.onboarding.config">
<div>
<nav class="onboarding-progress" aria-label="Onboarding progress">
<template x-for="item in $store.onboarding.steps" :key="item.step">
<div class="onboarding-progress-step"
:class="{ active: $store.onboarding.step === item.step, done: $store.onboarding.step > item.step }"
:aria-current="$store.onboarding.step === item.step ? 'step' : null">
<span class="onboarding-progress-dot">
<span x-show="$store.onboarding.step <= item.step" x-text="item.step"></span>
<span class="material-symbols-outlined onboarding-progress-check" x-show="$store.onboarding.step > item.step">check</span>
</span>
<span class="onboarding-progress-label" x-text="item.label"></span>
</div>
</template>
</nav>
<!-- Step 1: Main Model -->
<div x-show="$store.onboarding.step === 1">
<div class="onboarding-welcome-text">
<div class="onboarding-welcome-title">Welcome to Agent Zero</div>
First, choose the model Agent Zero will think with. The <b>Main Model</b> handles conversation, tool calls, skills, and browser work.
</div>
<div class="model-section">
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[0].title"></div>
<div class="section-description">Choose a capable general model for conversation, reasoning, tools, skills, and browser automation.</div>
<!-- Provider -->
<div class="field">
<div class="field-label">
<div class="field-title">Provider</div>
<div class="field-description">Where Agent Zero will send Main Model requests.</div>
</div>
<div class="field-control">
<select x-model="$store.onboarding.config.chat_model.provider"
x-effect="$nextTick(() => { if ($store.modelConfig.getProviders('chat_model').length) $el.value = $store.onboarding.config.chat_model.provider })">
<template x-for="p in $store.modelConfig.getProviders('chat_model')" :key="p.value">
<option :value="p.value" x-text="p.label"></option>
</template>
</select>
</div>
</div>
<!-- Model search -->
<div class="field">
<div class="field-label">
<div class="field-title">Model name</div>
<div class="field-description">Use the suggested model or search for another capable general-purpose model.</div>
</div>
<div class="field-control relative-container"
x-data="{ results: [], open: false, searching: false,
doSearch() { this.searching = true; $store.modelConfig.searchModels($store.onboarding.config.chat_model.provider, $store.onboarding.config.chat_model.name, $store.modelConfig.getSearchType('chat_model'), $store.onboarding.config.chat_model.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
grouped() { return $store.modelConfig.groupResults(this.results, $store.onboarding.config.chat_model.name); }
}"
@click.outside="open = false">
<input type="text" x-model="$store.onboarding.config.chat_model.name" class="input-with-icon" @keydown.enter.prevent="doSearch()" />
<span class="model-search-btn" @click="if (!searching) doSearch()" title="Search available models">
<span class="material-symbols-outlined" :style="searching && 'opacity:0'">search</span>
<span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
</span>
<div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
<template x-for="m in grouped().matched" :key="'m_'+m">
<div class="model-search-item matched" @click="$store.onboarding.config.chat_model.name = m; open = false;" x-text="m"></div>
</template>
<div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
<template x-for="m in grouped().rest" :key="'r_'+m">
<div class="model-search-item" @click="$store.onboarding.config.chat_model.name = m; open = false;" x-text="m"></div>
</template>
</div>
<div class="model-search-results" x-show="open && results.length === 0 && !searching">
<div class="model-search-item disabled">No models found</div>
</div>
</div>
</div>
<!-- API Key -->
<div class="field">
<div class="field-label">
<div class="field-title" x-text="`${$store.onboarding.providerLabel('chat_model')} API key`"></div>
<div class="field-description">Saved keys stay hidden. Paste a new key only when you need to add or replace one.</div>
</div>
<div class="field-control relative-container" x-data="{ showKey: false }">
<input :type="showKey ? 'text' : 'password'"
x-model="$store.modelConfig.apiKeyValues[$store.onboarding.config.chat_model.provider]"
:placeholder="$store.modelConfig.apiKeyStatus[$store.onboarding.config.chat_model.provider] ? '••••••••••••' : ''"
autocomplete="off"
class="input-with-icon"
@input="$store.modelConfig.touchApiKey($store.onboarding.config.chat_model.provider)" />
<span class="material-symbols-outlined eye-toggle"
@click="
showKey = !showKey;
const prov = $store.onboarding.config.chat_model.provider;
if (showKey && !$store.modelConfig.apiKeyValues[prov] && $store.modelConfig.apiKeyStatus[prov]) {
$store.modelConfig.revealApiKey(prov).then(v => { if (v) $store.modelConfig.apiKeyValues[prov] = v; });
}
"
x-text="showKey ? 'visibility' : 'visibility_off'"></span>
</div>
</div>
</div>
</div>
<!-- Step 2: Utility Model -->
<div x-show="$store.onboarding.step === 2">
<div class="onboarding-welcome-text">
<div class="onboarding-welcome-title">Choose your Utility Model</div>
Agent Zero uses the <b>Utility Model</b> for quiet work: summaries, memory, and preparation. Pick something fast, reliable, and cost-conscious.
</div>
<div class="model-section">
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[1].title"></div>
<div class="section-description">Choose a fast, reliable model for summaries, memory, and prompt preparation.</div>
<!-- Provider -->
<div class="field">
<div class="field-label">
<div class="field-title">Provider</div>
<div class="field-description">Use the same provider as your Main Model, or choose a faster low-cost option.</div>
</div>
<div class="field-control">
<select x-model="$store.onboarding.config.utility_model.provider"
x-effect="$nextTick(() => { if ($store.modelConfig.getProviders('utility_model').length) $el.value = $store.onboarding.config.utility_model.provider })">
<template x-for="p in $store.modelConfig.getProviders('utility_model')" :key="p.value">
<option :value="p.value" x-text="p.label"></option>
</template>
</select>
</div>
</div>
<!-- Model search -->
<div class="field">
<div class="field-label">
<div class="field-title">Model name</div>
<div class="field-description">The suggested model is tuned for speed and cost. Search if your provider uses another name.</div>
</div>
<div class="field-control relative-container"
x-data="{ results: [], open: false, searching: false,
doSearch() { this.searching = true; $store.modelConfig.searchModels($store.onboarding.config.utility_model.provider, $store.onboarding.config.utility_model.name, $store.modelConfig.getSearchType('utility_model'), $store.onboarding.config.utility_model.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
grouped() { return $store.modelConfig.groupResults(this.results, $store.onboarding.config.utility_model.name); }
}"
@click.outside="open = false">
<input type="text" x-model="$store.onboarding.config.utility_model.name" class="input-with-icon" @keydown.enter.prevent="doSearch()" />
<span class="model-search-btn" @click="if (!searching) doSearch()" title="Search available models">
<span class="material-symbols-outlined" :style="searching && 'opacity:0'">search</span>
<span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
</span>
<div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
<template x-for="m in grouped().matched" :key="'m_'+m">
<div class="model-search-item matched" @click="$store.onboarding.config.utility_model.name = m; open = false;" x-text="m"></div>
</template>
<div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
<template x-for="m in grouped().rest" :key="'r_'+m">
<div class="model-search-item" @click="$store.onboarding.config.utility_model.name = m; open = false;" x-text="m"></div>
</template>
</div>
<div class="model-search-results" x-show="open && results.length === 0 && !searching">
<div class="model-search-item disabled">No models found</div>
</div>
</div>
</div>
<!-- API Key -->
<div class="field">
<div class="field-label">
<div class="field-title" x-text="`${$store.onboarding.providerLabel('utility_model')} API key`"></div>
<div class="field-description">Saved keys stay hidden. Paste a new key only when you need to add or replace one.</div>
</div>
<div class="field-control relative-container" x-data="{ showKey: false }">
<input :type="showKey ? 'text' : 'password'"
x-model="$store.modelConfig.apiKeyValues[$store.onboarding.config.utility_model.provider]"
:placeholder="$store.modelConfig.apiKeyStatus[$store.onboarding.config.utility_model.provider] ? '••••••••••••' : ''"
autocomplete="off"
class="input-with-icon"
@input="$store.modelConfig.touchApiKey($store.onboarding.config.utility_model.provider)" />
<span class="material-symbols-outlined eye-toggle"
@click="
showKey = !showKey;
const prov = $store.onboarding.config.utility_model.provider;
if (showKey && !$store.modelConfig.apiKeyValues[prov] && $store.modelConfig.apiKeyStatus[prov]) {
$store.modelConfig.revealApiKey(prov).then(v => { if (v) $store.modelConfig.apiKeyValues[prov] = v; });
}
"
x-text="showKey ? 'visibility' : 'visibility_off'"></span>
</div>
</div>
</div>
</div>
<!-- Step 3: Success -->
<div x-show="$store.onboarding.step === 3" class="onboarding-success">
<div class="material-symbols-outlined onboarding-success-icon">check_circle</div>
<div class="onboarding-welcome-text onboarding-success-text">
<div class="onboarding-welcome-title">Agent Zero is ready</div>
Your models are configured. You can change them anytime in Settings.<br>
You can also connect integrations now and give Agent Zero more places to work.
</div>
<x-extension id="onboarding-success-end"></x-extension>
</div>
<div class="onboarding-advanced-link" x-show="$store.onboarding.step < 3">
<a href="#" @click.prevent="$store.onboarding.openAdvancedSettings()">
Advanced Settings <span class="material-symbols-outlined onboarding-advanced-link-icon">arrow_drop_down</span>
</a>
</div>
</div>
</template>
</div>
</div>
<div class="modal-footer" data-modal-footer>
<div class="onboarding-footer-left">
<button class="btn btn-cancel" @click="window.closeModal()" :disabled="$store.onboarding.loading">Cancel</button>
</div>
<div class="onboarding-footer-right">
<button class="btn" x-show="$store.onboarding.step > 1" @click="$store.onboarding.prev()" :disabled="$store.onboarding.loading">
Back
</button>
<button class="btn btn-ok" x-show="$store.onboarding.step < 3" @click="$store.onboarding.next()" :disabled="$store.onboarding.loading">
<span x-text="$store.onboarding.nextButtonLabel()"></span>
<span class="material-symbols-outlined onboarding-icon-right">arrow_forward</span>
</button>
<button class="btn btn-ok" x-show="$store.onboarding.step === 3" @click="$store.onboarding.finish()" :disabled="$store.onboarding.loading">
Start Chatting
</button>
</div>
</div>
</div>
</template>
</div>
</body>
</html>