mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-04-28 03:30:23 +00:00
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.
This commit is contained in:
parent
0bbc657dd3
commit
0061b3a511
13 changed files with 1217 additions and 15 deletions
|
|
@ -0,0 +1,77 @@
|
|||
from helpers.extension import Extension
|
||||
from helpers import plugins
|
||||
|
||||
class DiscoveryCardsExtension(Extension):
|
||||
"""Injects discovery cards into the banners list."""
|
||||
|
||||
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
|
||||
# Optional logic: only show specific cards if plugins aren't already configured.
|
||||
# Telegram, Email, Whatsapp are built-in, so we only need to check if they've been configured.
|
||||
|
||||
telegram_config = plugins.get_plugin_config("_telegram_integration") or {}
|
||||
email_config = plugins.get_plugin_config("_email_integration") or {}
|
||||
whatsapp_config = plugins.get_plugin_config("_whatsapp_integration") or {}
|
||||
|
||||
# 1. Plugin Hub Hero
|
||||
banners.append({
|
||||
"id": "discovery-plugin-hub",
|
||||
"type": "hero",
|
||||
"title": "Discover the Plugin Hub",
|
||||
"description": "Extend Agent Zero with integrations, tools, and automations from the community.",
|
||||
"thumbnail": "/plugins/_discovery/webui/assets/hero-plugin-hub.png",
|
||||
"icon": "extension",
|
||||
"cta_text": "Explore Plugins",
|
||||
"cta_action": "open-plugin-hub",
|
||||
"dismissible": True,
|
||||
"priority": 100,
|
||||
"show_in_onboarding": True
|
||||
})
|
||||
|
||||
# 2. Telegram
|
||||
if not telegram_config.get("bot_token"):
|
||||
banners.append({
|
||||
"id": "discovery-telegram",
|
||||
"type": "feature",
|
||||
"title": "Connect Telegram",
|
||||
"description": "Chat with Agent Zero from Telegram wherever you are.",
|
||||
"thumbnail": "/plugins/_discovery/webui/assets/thumb-telegram.png",
|
||||
"icon": "send",
|
||||
"cta_text": "Setup",
|
||||
"cta_action": "open-plugin-config:_telegram_integration",
|
||||
"dismissible": True,
|
||||
"priority": 50,
|
||||
"show_in_onboarding": True
|
||||
})
|
||||
|
||||
# 3. Email
|
||||
if not email_config.get("imap_username") and not email_config.get("smtp_username"):
|
||||
banners.append({
|
||||
"id": "discovery-email",
|
||||
"type": "feature",
|
||||
"title": "Setup Email",
|
||||
"description": "Let Agent Zero read and send emails on your behalf.",
|
||||
"thumbnail": "/plugins/_discovery/webui/assets/thumb-email.png",
|
||||
"icon": "mail",
|
||||
"cta_text": "Setup",
|
||||
"cta_action": "open-plugin-config:_email_integration",
|
||||
"dismissible": True,
|
||||
"priority": 50,
|
||||
"show_in_onboarding": True
|
||||
})
|
||||
|
||||
# 4. WhatsApp
|
||||
if not whatsapp_config.get("phone_number_id"):
|
||||
banners.append({
|
||||
"id": "discovery-whatsapp",
|
||||
"type": "feature",
|
||||
"title": "Connect WhatsApp",
|
||||
"description": "Send and receive WhatsApp messages through A0.",
|
||||
"thumbnail": "/plugins/_discovery/webui/assets/thumb-whatsapp.png",
|
||||
"icon": "chat",
|
||||
"cta_text": "Setup",
|
||||
"cta_action": "open-plugin-config:_whatsapp_integration",
|
||||
"dismissible": True,
|
||||
"priority": 50,
|
||||
"show_in_onboarding": True
|
||||
})
|
||||
|
||||
|
|
@ -0,0 +1,483 @@
|
|||
<script type="module">
|
||||
import { store } from "/plugins/_discovery/webui/discovery-store.js";
|
||||
</script>
|
||||
|
||||
<div x-data>
|
||||
<template x-if="$store.discoveryStore">
|
||||
<div class="discovery-slot"
|
||||
@modal-closed.window="$store.discoveryStore.refreshCards()"
|
||||
x-create="$store.discoveryStore.refreshCards()"
|
||||
x-show="$store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards">
|
||||
|
||||
<div class="discovery-section" style="margin-top: 2rem;">
|
||||
|
||||
<div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0" style="text-align: left;">
|
||||
<template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
|
||||
<article class="discovery-feature-card"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<button class="discovery-dismiss discovery-dismiss-small"
|
||||
type="button"
|
||||
x-show="card.dismissible"
|
||||
@click.stop="$store.discoveryStore.dismissCard(card.id)"
|
||||
:aria-label="`Dismiss ${card.title}`"
|
||||
title="Dismiss">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
|
||||
<div class="discovery-feature-head">
|
||||
<div class="discovery-feature-thumb">
|
||||
<template x-if="card.thumbnail">
|
||||
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!card.thumbnail && card.icon">
|
||||
<span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
|
||||
</template>
|
||||
</div>
|
||||
<h4 class="discovery-feature-title" x-text="card.title"></h4>
|
||||
</div>
|
||||
|
||||
<p class="discovery-feature-desc" x-text="card.description"></p>
|
||||
|
||||
<button class="btn btn primary"
|
||||
type="button"
|
||||
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<span x-text="card.cta_text"></span>
|
||||
<span class="material-symbols-outlined">arrow_forward</span>
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
<template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
|
||||
<article class="discovery-hero"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<button class="discovery-dismiss"
|
||||
type="button"
|
||||
x-show="card.dismissible"
|
||||
@click.stop="$store.discoveryStore.dismissCard(card.id)"
|
||||
:aria-label="`Dismiss ${card.title}`"
|
||||
title="Dismiss">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
|
||||
<div class="discovery-hero-content">
|
||||
<h3 class="discovery-hero-title" x-text="card.title"></h3>
|
||||
<p class="discovery-hero-desc" x-text="card.description"></p>
|
||||
<button class="btn btn-ok"
|
||||
type="button"
|
||||
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<span x-text="card.cta_text"></span>
|
||||
<span class="material-symbols-outlined">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="discovery-hero-thumb">
|
||||
<template x-if="card.thumbnail">
|
||||
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!card.thumbnail && card.icon">
|
||||
<span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
|
||||
<button type="button"
|
||||
class="discovery-undismiss-btn"
|
||||
@click="$store.discoveryStore.undismissCards()">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
<span>Show dismissed suggestions</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.discovery-slot {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.discovery-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.discovery-hero,
|
||||
.discovery-feature-card {
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-panel);
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.discovery-hero:hover,
|
||||
.discovery-feature-card:hover,
|
||||
.discovery-hero:focus-visible,
|
||||
.discovery-feature-card:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 152px;
|
||||
overflow: hidden;
|
||||
padding: 1.25rem 1.25rem 1.25rem 1.5rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--color-primary) 10%, transparent),
|
||||
transparent 58%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 92% 24%,
|
||||
color-mix(in srgb, var(--color-primary) 12%, transparent),
|
||||
transparent 42%
|
||||
),
|
||||
var(--color-panel);
|
||||
}
|
||||
|
||||
.discovery-hero-content,
|
||||
.discovery-hero-thumb {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.discovery-hero-content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-hero-title {
|
||||
margin: 0 0 0.4rem;
|
||||
color: var(--color-text);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.discovery-hero-desc {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-text);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
max-width: 38ch;
|
||||
}
|
||||
|
||||
.discovery-cta-link,
|
||||
.discovery-undismiss-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
flex: 0 0 132px;
|
||||
width: 132px;
|
||||
height: 108px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.discovery-hero-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--color-highlight-dark);
|
||||
}
|
||||
|
||||
.discovery-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 188px;
|
||||
padding: 1rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-primary) 4%, transparent),
|
||||
transparent 38%
|
||||
),
|
||||
var(--color-panel);
|
||||
}
|
||||
|
||||
.discovery-feature-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
flex: 0 0 auto;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.discovery-feature-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.discovery-feature-icon {
|
||||
font-size: 1.45rem;
|
||||
color: var(--color-highlight-dark);
|
||||
}
|
||||
|
||||
.discovery-feature-title {
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
margin: 0.55rem 0 0;
|
||||
padding-bottom: var(--spacing-md);
|
||||
color: var(--color-text);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.discovery-cta-link {
|
||||
margin-top: 0.95rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
padding: 0.42rem 0.72rem;
|
||||
font-size: 0.79rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.discovery-cta-link:hover,
|
||||
.discovery-cta-link:focus-visible {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-dismiss {
|
||||
position: absolute;
|
||||
top: 0.65rem;
|
||||
right: 0.65rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.discovery-dismiss-small {
|
||||
top: 0.55rem;
|
||||
right: 0.55rem;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.discovery-hero:hover .discovery-dismiss,
|
||||
.discovery-hero:focus-visible .discovery-dismiss,
|
||||
.discovery-feature-card:hover .discovery-dismiss,
|
||||
.discovery-feature-card:focus-visible .discovery-dismiss {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.discovery-dismiss:hover,
|
||||
.discovery-dismiss:focus-visible {
|
||||
opacity: 1;
|
||||
color: var(--color-text);
|
||||
background: color-mix(in srgb, var(--color-border) 84%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-dismiss .material-symbols-outlined {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discovery-undismiss {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.discovery-undismiss-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.discovery-undismiss-btn:hover,
|
||||
.discovery-undismiss-btn:focus-visible {
|
||||
color: var(--color-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-cta-link .material-symbols-outlined,
|
||||
.discovery-undismiss-btn .material-symbols-outlined {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.discovery-dismiss {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.discovery-section {
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
flex-basis: 92px;
|
||||
width: 92px;
|
||||
height: 76px;
|
||||
}
|
||||
|
||||
.discovery-features {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-head {
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.discovery-cta-link {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.discovery-slot {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
padding-right: 0.95rem;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discovery-hero-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
<script type="module">
|
||||
import { store } from "/plugins/_discovery/webui/discovery-store.js";
|
||||
</script>
|
||||
|
||||
<div x-data>
|
||||
<template x-if="$store.discoveryStore">
|
||||
<div class="discovery-slot"
|
||||
@modal-closed.window="$store.discoveryStore.refreshCards()"
|
||||
x-create="$store.discoveryStore.refreshCards()"
|
||||
x-show="!($store.welcomeStore?.banners || []).some(b => b.id === 'missing-api-key') && ($store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards)">
|
||||
|
||||
<div class="discovery-section">
|
||||
<template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
|
||||
<article class="discovery-hero"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<button class="discovery-dismiss"
|
||||
type="button"
|
||||
x-show="card.dismissible"
|
||||
@click.stop="$store.discoveryStore.dismissCard(card.id)"
|
||||
:aria-label="`Dismiss ${card.title}`"
|
||||
title="Dismiss">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
|
||||
<div class="discovery-hero-content">
|
||||
<h3 class="discovery-hero-title" x-text="card.title"></h3>
|
||||
<p class="discovery-hero-desc" x-text="card.description"></p>
|
||||
<button class="btn btn-ok"
|
||||
type="button"
|
||||
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<span x-text="card.cta_text"></span>
|
||||
<span class="material-symbols-outlined">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="discovery-hero-thumb">
|
||||
<template x-if="card.thumbnail">
|
||||
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!card.thumbnail && card.icon">
|
||||
<span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0">
|
||||
<template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
|
||||
<article class="discovery-feature-card"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
|
||||
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<button class="discovery-dismiss discovery-dismiss-small"
|
||||
type="button"
|
||||
x-show="card.dismissible"
|
||||
@click.stop="$store.discoveryStore.dismissCard(card.id)"
|
||||
:aria-label="`Dismiss ${card.title}`"
|
||||
title="Dismiss">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
|
||||
<div class="discovery-feature-head">
|
||||
<div class="discovery-feature-thumb">
|
||||
<template x-if="card.thumbnail">
|
||||
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!card.thumbnail && card.icon">
|
||||
<span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
|
||||
</template>
|
||||
</div>
|
||||
<h4 class="discovery-feature-title" x-text="card.title"></h4>
|
||||
</div>
|
||||
|
||||
<p class="discovery-feature-desc" x-text="card.description"></p>
|
||||
|
||||
<button class="btn btn primary"
|
||||
type="button"
|
||||
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
|
||||
<span x-text="card.cta_text"></span>
|
||||
<span class="material-symbols-outlined">arrow_forward</span>
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
|
||||
<button type="button"
|
||||
class="discovery-undismiss-btn"
|
||||
@click="$store.discoveryStore.undismissCards()">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
<span>Show dismissed suggestions</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.discovery-slot {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.discovery-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.discovery-hero,
|
||||
.discovery-feature-card {
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-panel);
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.discovery-hero:hover,
|
||||
.discovery-feature-card:hover,
|
||||
.discovery-hero:focus-visible,
|
||||
.discovery-feature-card:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 152px;
|
||||
overflow: hidden;
|
||||
padding: 1.25rem 1.25rem 1.25rem 1.5rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--color-primary) 10%, transparent),
|
||||
transparent 58%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 92% 24%,
|
||||
color-mix(in srgb, var(--color-primary) 12%, transparent),
|
||||
transparent 42%
|
||||
),
|
||||
var(--color-panel);
|
||||
}
|
||||
|
||||
.discovery-hero-content,
|
||||
.discovery-hero-thumb {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.discovery-hero-content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-hero-title {
|
||||
margin: 0 0 0.4rem;
|
||||
color: var(--color-text);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.discovery-hero-desc {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-text);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
max-width: 38ch;
|
||||
}
|
||||
|
||||
.discovery-cta-link,
|
||||
.discovery-undismiss-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
flex: 0 0 132px;
|
||||
width: 132px;
|
||||
height: 108px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.discovery-hero-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--color-highlight-dark);
|
||||
}
|
||||
|
||||
.discovery-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 188px;
|
||||
padding: 1rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-primary) 4%, transparent),
|
||||
transparent 38%
|
||||
),
|
||||
var(--color-panel);
|
||||
}
|
||||
|
||||
.discovery-feature-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
flex: 0 0 auto;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.discovery-feature-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.discovery-feature-icon {
|
||||
font-size: 1.45rem;
|
||||
color: var(--color-highlight-dark);
|
||||
}
|
||||
|
||||
.discovery-feature-title {
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
margin: 0.55rem 0 0;
|
||||
padding-bottom: var(--spacing-md);
|
||||
color: var(--color-text);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.discovery-cta-link {
|
||||
margin-top: 0.95rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
padding: 0.42rem 0.72rem;
|
||||
font-size: 0.79rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.discovery-cta-link:hover,
|
||||
.discovery-cta-link:focus-visible {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-dismiss {
|
||||
position: absolute;
|
||||
top: 0.65rem;
|
||||
right: 0.65rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.discovery-dismiss-small {
|
||||
top: 0.55rem;
|
||||
right: 0.55rem;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.discovery-hero:hover .discovery-dismiss,
|
||||
.discovery-hero:focus-visible .discovery-dismiss,
|
||||
.discovery-feature-card:hover .discovery-dismiss,
|
||||
.discovery-feature-card:focus-visible .discovery-dismiss {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.discovery-dismiss:hover,
|
||||
.discovery-dismiss:focus-visible {
|
||||
opacity: 1;
|
||||
color: var(--color-text);
|
||||
background: color-mix(in srgb, var(--color-border) 84%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-dismiss .material-symbols-outlined {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discovery-undismiss {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.discovery-undismiss-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.discovery-undismiss-btn:hover,
|
||||
.discovery-undismiss-btn:focus-visible {
|
||||
color: var(--color-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discovery-cta-link .material-symbols-outlined,
|
||||
.discovery-undismiss-btn .material-symbols-outlined {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.discovery-dismiss {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.discovery-section {
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
flex-basis: 92px;
|
||||
width: 92px;
|
||||
height: 76px;
|
||||
}
|
||||
|
||||
.discovery-features {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-head {
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.discovery-cta-link {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.discovery-slot {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.discovery-hero {
|
||||
padding-right: 0.95rem;
|
||||
}
|
||||
|
||||
.discovery-hero-thumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discovery-hero-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
8
plugins/_discovery/plugin.yaml
Normal file
8
plugins/_discovery/plugin.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
name: _discovery
|
||||
title: Plugin Discovery
|
||||
description: Contextual discovery cards on the welcome screen promoting plugins and integrations.
|
||||
version: 1.0.0
|
||||
settings_sections: []
|
||||
always_enabled: true
|
||||
per_project_config: false
|
||||
per_agent_config: false
|
||||
BIN
plugins/_discovery/webui/assets/hero-plugin-hub.png
Normal file
BIN
plugins/_discovery/webui/assets/hero-plugin-hub.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
plugins/_discovery/webui/assets/thumb-email.png
Normal file
BIN
plugins/_discovery/webui/assets/thumb-email.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
plugins/_discovery/webui/assets/thumb-telegram.png
Normal file
BIN
plugins/_discovery/webui/assets/thumb-telegram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
plugins/_discovery/webui/assets/thumb-whatsapp.png
Normal file
BIN
plugins/_discovery/webui/assets/thumb-whatsapp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
129
plugins/_discovery/webui/discovery-store.js
Normal file
129
plugins/_discovery/webui/discovery-store.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { createStore } from "/js/AlpineStore.js";
|
||||
import { toastFrontendError } from "/components/notifications/notification-store.js";
|
||||
import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
|
||||
import * as API from "/js/api.js";
|
||||
|
||||
const STORAGE_KEY = "dismissed_discovery_cards";
|
||||
|
||||
const model = {
|
||||
// --- State ---
|
||||
/** @type {any[]} */
|
||||
cards: [],
|
||||
hasDismissedCards: false,
|
||||
_initialized: false,
|
||||
_isLoading: false,
|
||||
|
||||
// --- Lifecycle ---
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
async refreshCards() {
|
||||
if (this._isLoading) return;
|
||||
this._isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await API.callJsonApi("/banners", {
|
||||
banners: [],
|
||||
context: {
|
||||
is_onboarding: document.body.dataset.mode === "onboarding"
|
||||
},
|
||||
});
|
||||
|
||||
const banners = response?.banners || [];
|
||||
const dismissed = this._getDismissedIds();
|
||||
|
||||
// Filter out standard banners, keep only hero and feature
|
||||
// Also respect the onboarding filtering
|
||||
const is_onboarding = document.body.dataset.mode === "onboarding";
|
||||
|
||||
this.cards = banners
|
||||
.filter((card) => card.type === "hero" || card.type === "feature")
|
||||
.filter((card) => !is_onboarding || card.show_in_onboarding === true)
|
||||
.filter((card) => !dismissed.has(card.id))
|
||||
.sort((left, right) => (right.priority || 0) - (left.priority || 0));
|
||||
|
||||
this.hasDismissedCards = dismissed.size > 0;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch discovery cards:", error);
|
||||
} finally {
|
||||
this._isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
dismissCard(cardId) {
|
||||
const dismissed = this._getDismissedIds();
|
||||
dismissed.add(cardId);
|
||||
this._persistDismissedIds(dismissed);
|
||||
|
||||
// Optimistically update UI
|
||||
this.cards = this.cards.filter(c => c.id !== cardId);
|
||||
this.hasDismissedCards = true;
|
||||
},
|
||||
|
||||
undismissCards() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
this.refreshCards();
|
||||
},
|
||||
|
||||
async executeCta(action) {
|
||||
if (!action) return;
|
||||
|
||||
try {
|
||||
if (action === "open-plugin-hub") {
|
||||
await pluginListStore.open("pluginHub");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.startsWith("open-plugin-config:")) {
|
||||
const pluginName = action.split(":")[1];
|
||||
await pluginListStore.openPluginConfig(pluginName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.startsWith("open-url:")) {
|
||||
const url = action.slice("open-url:".length);
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Discovery action failed:", error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await toastFrontendError(message, "Discovery");
|
||||
}
|
||||
},
|
||||
|
||||
// --- Helpers (Private-ish) ---
|
||||
|
||||
_getDismissedIds() {
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||
if (!Array.isArray(raw)) return new Set();
|
||||
return new Set(raw);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
},
|
||||
|
||||
_persistDismissedIds(ids) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(ids)));
|
||||
},
|
||||
|
||||
// --- Computed ---
|
||||
|
||||
get heroCards() {
|
||||
return this.cards.filter((card) => card.type === "hero");
|
||||
},
|
||||
|
||||
get featureCards() {
|
||||
return this.cards.filter((card) => card.type === "feature");
|
||||
},
|
||||
};
|
||||
|
||||
const store = createStore("discoveryStore", model);
|
||||
export { store };
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
.onboarding-welcome-text {
|
||||
text-align: center;
|
||||
margin: var(--spacing-md) 0;
|
||||
margin: var(--spacing-md) 0 var(--spacing-lg) 0;
|
||||
color: var(--text-2);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
}
|
||||
.onboarding-success {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
.onboarding-success-icon {
|
||||
font-size: 64px;
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
<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.<br> We recommend a capable model like Claude Sonnet 4.6, GPT-5.4, Kimi 2.5, or similar.
|
||||
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">
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
<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.<br> A fast, cheap model like GPT-5.4-mini, Gemini 3.1 Flash Lite, or similar works best here.
|
||||
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">
|
||||
|
|
@ -353,10 +353,11 @@
|
|||
<!-- 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-title">Ready to chat!</div>
|
||||
<div class="onboarding-welcome-text onboarding-success-text">
|
||||
Your models are configured. You can change these anytime in Settings.
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
defaultPriority,
|
||||
} from "/components/notifications/notification-store.js";
|
||||
|
||||
const MODAL_PATH = "components/plugins/list/plugin-list.html";
|
||||
|
||||
// define the model object holding data and functions
|
||||
const model = {
|
||||
loading: false,
|
||||
|
|
@ -22,11 +24,22 @@ const model = {
|
|||
readmeLoading: false,
|
||||
readmeError: "",
|
||||
|
||||
async open(tab = "custom") {
|
||||
await this.setTab(tab);
|
||||
window.openModal?.(MODAL_PATH);
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.loading = false;
|
||||
await this.setTab('custom');
|
||||
if (this.plugins.length === 0) {
|
||||
await this.setTab('builtin');
|
||||
// If a tab is already selected (e.g. via open()), use it.
|
||||
// Otherwise default to custom -> builtin fallback.
|
||||
if (this.activeTab && this.activeTab !== "custom") {
|
||||
await this.setTab(this.activeTab);
|
||||
} else {
|
||||
await this.setTab("custom");
|
||||
if (this.plugins.length === 0) {
|
||||
await this.setTab("builtin");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -81,13 +94,21 @@ const model = {
|
|||
pluginExecuteStore.open(plugin);
|
||||
},
|
||||
|
||||
async openPluginConfig(plugin) {
|
||||
if (!plugin?.name || !plugin?.has_config_screen) return;
|
||||
async openPluginConfig(pluginOrName) {
|
||||
const pluginName =
|
||||
typeof pluginOrName === "string" ? pluginOrName : pluginOrName?.name;
|
||||
if (!pluginName) return;
|
||||
|
||||
// If it's an object, we can check has_config_screen.
|
||||
// If it's a name, we just try to open it and let pluginSettingsStore handle errors.
|
||||
if (typeof pluginOrName === "object" && !pluginOrName.has_config_screen)
|
||||
return;
|
||||
|
||||
try {
|
||||
if (!pluginSettingsStore?.openConfig) {
|
||||
throw new Error("Plugin settings store is unavailable.");
|
||||
}
|
||||
await pluginSettingsStore.openConfig(plugin.name);
|
||||
await pluginSettingsStore.openConfig(pluginName);
|
||||
} catch (e) {
|
||||
showErrorNotification(e, "Failed to open plugin config");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,9 +141,9 @@ const model = {
|
|||
},
|
||||
|
||||
get sortedBanners() {
|
||||
return [...this.banners].sort(
|
||||
(a, b) => (b.priority || 0) - (a.priority || 0),
|
||||
);
|
||||
return [...this.banners]
|
||||
.filter((b) => b.type !== "hero" && b.type !== "feature")
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@ some classes like modal-header are shared between the old and the new system */
|
|||
background: #2196f3;
|
||||
color: white;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue