agent-zero/plugins/_discovery/webui/discovery-store.js
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

129 lines
3.4 KiB
JavaScript

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 };