mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 16:31:30 +00:00
Merge pull request #1402 from 3clyp50/discovery
feat: add plugin discovery to dashboard and onboarding wizard
This commit is contained in:
commit
01de1f7bca
15 changed files with 1313 additions and 16 deletions
|
|
@ -7,7 +7,7 @@ Tech Stack: Python 3.12+ | Flask | Alpine.js | LiteLLM | WebSocket (Socket.io)
|
|||
Dev Server: python run_ui.py (runs on http://localhost:50001 by default)
|
||||
Run Tests: pytest (standard) or pytest tests/test_name.py (file-scoped)
|
||||
Documentation: README.md | docs/
|
||||
Frontend Deep Dives: [Component System](docs/agents/AGENTS.components.md) | [Modal System](docs/agents/AGENTS.modals.md) | [Plugin Architecture](docs/agents/AGENTS.plugins.md)
|
||||
Frontend Deep Dives: [Component System](docs/agents/AGENTS.components.md) | [Modal System](docs/agents/AGENTS.modals.md) | [Plugin Architecture](docs/agents/AGENTS.plugins.md) | [Banners & Discovery](docs/agents/AGENTS.banners.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
95
docs/agents/AGENTS.banners.md
Normal file
95
docs/agents/AGENTS.banners.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Creating Discovery Cards and Banners
|
||||
|
||||
Agent Zero allows plugin developers to surface UI elements using the `banners` extension point. This allows your plugin to present information, prompts, or actionable "discovery cards" directly on the Welcome Screen without needing to inject arbitrary HTML into the frontend.
|
||||
|
||||
## The `banners` Extension Point
|
||||
|
||||
Banners are collected on the backend and sent to the frontend UI as an array of dictionaries. By appending to the `banners` array inside a Python extension, you can easily surface your plugin to the user.
|
||||
|
||||
To inject a banner, you create a Python extension script hooking into `banners`.
|
||||
|
||||
### Where to put your extension script
|
||||
|
||||
Create a python file in your plugin's extensions folder:
|
||||
`plugins/<your_plugin>/extensions/python/banners/10_my_plugin_banner.py`
|
||||
|
||||
*(Note: the `10_` prefix is for ordering; extensions run in alphabetical order).*
|
||||
|
||||
## Banner Types
|
||||
|
||||
The UI distinguishes banners primarily by the `type` property.
|
||||
|
||||
### 1. Alert Banners (`info`, `warning`, `error`)
|
||||
These are standard top-level alerts displayed on the welcome screen.
|
||||
|
||||
```python
|
||||
banners.append({
|
||||
"id": "my-plugin-warning",
|
||||
"type": "warning",
|
||||
"priority": 90,
|
||||
"title": "My Plugin Issue",
|
||||
"html": "<strong>Action required:</strong> Please configure your settings.",
|
||||
"dismissible": True,
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Discovery Cards (`hero`, `feature`)
|
||||
These are rich, interactive cards displayed in the Discovery section. They are designed to prompt the user to try new plugins or features.
|
||||
|
||||
* `hero`: A wide, prominent card. Usually reserved for core system features (e.g., the Plugin Hub).
|
||||
* `feature`: A smaller card in a grid layout. This is the **recommended type** for plugin contributors to showcase their plugin.
|
||||
|
||||
### Anatomy of a Discovery Card
|
||||
|
||||
Here is an example of injecting a `feature` card for a custom plugin:
|
||||
|
||||
```python
|
||||
from helpers.extension import Extension
|
||||
from helpers import plugins
|
||||
|
||||
class MyPluginDiscoveryCard(Extension):
|
||||
"""Injects a discovery card for My Custom Plugin."""
|
||||
|
||||
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
|
||||
# 1. Condition Check
|
||||
# Only show the discovery card if the user hasn't configured the plugin yet.
|
||||
config = plugins.get_plugin_config("my_custom_plugin") or {}
|
||||
|
||||
# If the API key is already set, we don't need to advertise the setup!
|
||||
if config.get("api_key"):
|
||||
return
|
||||
|
||||
# 2. Add the Card
|
||||
banners.append({
|
||||
"id": "discovery-my-custom-plugin",
|
||||
"type": "feature", # 'feature' or 'hero'
|
||||
"title": "Connect My Service", # Card title
|
||||
"description": "Unlock amazing capabilities by linking your account.",
|
||||
|
||||
# Visuals (use either thumbnail OR icon)
|
||||
"thumbnail": "/plugins/my_custom_plugin/assets/thumb.png", # Path to image
|
||||
"icon": "bolt", # Or a Material Symbol icon name
|
||||
|
||||
# Call To Action (CTA)
|
||||
"cta_text": "Setup Now",
|
||||
"cta_action": "open-plugin-config:my_custom_plugin", # Opens your plugin's config modal
|
||||
|
||||
# Behavior
|
||||
"dismissible": True, # Let the user hide it
|
||||
"priority": 40, # Higher numbers appear first
|
||||
})
|
||||
```
|
||||
|
||||
## Call To Action (CTA) Actions
|
||||
|
||||
When a user clicks the button on a discovery card, the `cta_action` string determines what happens. The frontend currently supports the following actions:
|
||||
|
||||
* `open-plugin-config:<plugin_folder_name>`: Automatically opens the settings modal for the specified plugin. (e.g., `open-plugin-config:_telegram_integration`).
|
||||
* `open-plugin-hub`: Opens the main Plugin Hub UI.
|
||||
* `open-url:<url>`: Opens a web link in a new browser tab. (e.g., `open-url:https://example.com/docs`).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Check Configuration First**: Always check your plugin's configuration before injecting a card. If the user has already set up your plugin, they shouldn't keep seeing a discovery card asking them to set it up.
|
||||
2. **Unique IDs**: Ensure your banner `id` is highly unique (e.g., prefix it with your plugin name) to avoid collisions with other plugins.
|
||||
3. **Use `feature` type**: Community plugins should stick to the `feature` type rather than `hero` to ensure a clean grid layout for users.
|
||||
|
|
@ -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