agent-zero/plugins/_onboarding/webui/onboarding.html
Alessandro 0061b3a511 feat: add built-in plugin discovery cards to the welcome screen
Add the always-enabled `_discovery` plugin to turn the welcome screen into a discovery surface for the Plugin Hub and A0 integrations.

Includes a hero card plus Telegram, Email, and WhatsApp feature cards, with persistent dismiss/restore state, CTA routing to plugin config screens, and self-contained placeholder artwork. Implemented entirely through the existing WebUI extension mechanism with no core welcome-screen changes.

stores cleanup

layout polish and onboarding integration

Move feature card titles beside thumbnails for better space efficiency
and visibility. Restructure card markup and styles to support a fluid
grid layout and horizontal alignment.

Integrate discovery cards into the final onboarding step via a new
'onboarding-success-end' extension point, ensuring new users see
extension opportunities immediately after setup.

Hide discovery cards on the dashboard while the missing API key
onboarding banner is visible to reduce UI noise and user confusion during initial config.

update discovery card initialization and loading logic

Enhance the discovery store to fetch cards from the API, improving the dynamic loading of discovery cards based on user context. This change optimizes the user experience by ensuring relevant cards are displayed immediately after onboarding and when modals are closed.

And on top of that, there's a proper backend for these new cards.
2026-03-31 19:59:16 +02:00

397 lines
No EOL
24 KiB
HTML

<html>
<head>
<title>Welcome to Agent Zero</title>
<script type="module">
import { store } from "/plugins/_onboarding/webui/onboarding-store.js";
</script>
<style>
.onboarding-logo {
text-align: center;
margin-bottom: 24px;
}
.onboarding-logo img {
width: 200px;
max-width: 100%;
height: auto;
}
.onboarding-welcome-text {
text-align: center;
margin: var(--spacing-md) 0 var(--spacing-lg) 0;
color: var(--text-2);
font-size: 1.1rem;
line-height: 1.5;
}
.onboarding-welcome-title {
color: var(--text-1);
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 12px;
}
.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 {
background: var(--surface-2);
border-radius: 8px;
border: 1px solid var(--surface-3);
}
.onboarding-body .section-title { margin-bottom: 8px; }
.onboarding-body .section-description { margin-bottom: 24px; }
.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;
}
</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>
<!-- 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>
Let's get your models configured. The <b>Main Model</b> handles chat, tool calls, skills, and browser automation. We recommend a capable model like Claude Sonnet 4.6, GPT-5.4, Kimi 2.5, or similar.
</div>
<div class="model-section">
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[0].title"></div>
<div class="section-description" x-text="$store.modelConfig.MODEL_SECTIONS[0].desc"></div>
<!-- Provider -->
<div class="field">
<div class="field-label">
<div class="field-title">Provider</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">Model identifier. Click the search icon to browse available models.</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">API key</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">Almost there!</div>
The <b>Utility Model</b> handles background tasks like summarization and memory updates. A fast, cheap model like GPT-5.4-mini, Gemini 3.1 Flash Lite, or similar works best here.
</div>
<div class="model-section">
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[1].title"></div>
<div class="section-description" x-text="$store.modelConfig.MODEL_SECTIONS[1].desc"></div>
<!-- Provider -->
<div class="field">
<div class="field-label">
<div class="field-title">Provider</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">Model identifier. Click the search icon to browse available models.</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">API key</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">
Your models are configured. You can change these anytime in Settings.<br>
While you're here, why not check out some integrations to extend what Agent Zero can do?
</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">
Next <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>