Refine welcome screen setup surfaces
Some checks are pending
Build And Publish Docker Images / plan (push) Waiting to run
Build And Publish Docker Images / build (push) Blocked by required conditions

Fold the blocking onboarding state into the welcome composer, refresh the account and channel tiles, and keep system resources aligned with the renewed Welcome Screen layout.

Update model-config, OAuth, and welcome static tests to pin the new CTA, dismissal, copy, and layout contracts.
This commit is contained in:
Alessandro 2026-06-23 02:21:44 +02:00
parent 0e262f7b87
commit 489acfe8c6
10 changed files with 555 additions and 185 deletions

View file

@ -138,7 +138,7 @@ class DiscoveryCardsExtension(Extension):
"type": "hero",
"placement": "after-features",
"title": "Your AI accounts",
"description": "Link account-backed providers such as ChatGPT/Codex, GitHub Copilot, Gemini API, and xAI Grok."
"description": "Use your subscription-backed logins for model access."
if not connected_count
else "",
"cta_text": "Connect accounts" if not connected_count else "Manage accounts",

View file

@ -276,8 +276,8 @@
.discovery-feature-thumb {
flex: 0 0 auto;
width: 44px;
height: 44px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
@ -570,8 +570,8 @@
}
.discovery-feature-thumb {
width: 46px;
height: 46px;
width: 32px;
height: 32px;
}
.discovery-feature-desc {

View file

@ -11,37 +11,35 @@
<section class="discovery-features-panel" x-show="$store.discoveryStore.featureCards.length > 0">
<header class="discovery-panel-header">
<h2>Connect Channels</h2>
<p>Extend Agent Zero across your favorite platforms.</p>
<div class="discovery-panel-copy">
<h2>Connect Channels</h2>
<p>Extend Agent Zero across your favorite platforms.</p>
</div>
<button class="discovery-dismiss discovery-panel-dismiss"
type="button"
x-show="$store.discoveryStore.featureCards.some(card => card.dismissible !== false)"
@click.stop="$store.discoveryStore.dismissFeatureCards()"
aria-label="Dismiss Connect Channels"
title="Dismiss">
<span class="material-symbols-outlined">close</span>
</button>
</header>
<div class="discovery-features">
<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)">
<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>
<button class="btn btn primary"
type="button"
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
</button>
</article>
<button class="discovery-feature-card"
type="button"
@click="$store.discoveryStore.executeCta(card.cta_action)">
<span 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>
</span>
<span class="discovery-feature-title" x-text="`${card.cta_text || 'Connect'} ${card.title}`"></span>
</button>
</template>
</div>
</section>
@ -58,11 +56,16 @@
</button>
<header class="discovery-account-header">
<span class="material-symbols-outlined discovery-account-icon">key</span>
<div class="discovery-account-copy">
<h2 x-text="card.title"></h2>
<p x-show="card.description" x-text="card.description"></p>
</div>
<button class="btn btn primary discovery-account-cta"
type="button"
@click="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</header>
<div class="discovery-account-chips" x-show="(card.account_chips || []).length > 0">
@ -95,12 +98,6 @@
</template>
</div>
<button class="btn btn primary discovery-account-cta"
type="button"
@click="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</article>
</template>
@ -135,15 +132,23 @@
.discovery-features-panel {
min-height: 100%;
padding: 1.7rem 1.55rem 1.55rem;
padding: 1.25rem 1.45rem;
}
.discovery-panel-header {
display: grid;
gap: 0.42rem;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.7rem;
align-items: start;
margin-bottom: 1.28rem;
}
.discovery-panel-copy {
display: grid;
gap: 0.42rem;
min-width: 0;
}
.discovery-panel-header h2 {
margin: 0;
color: var(--color-text);
@ -162,31 +167,32 @@
.discovery-features {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.7rem;
gap: 0.65rem;
}
.discovery-feature-card {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.discovery-feature-head {
display: flex;
flex-direction: row;
display: inline-flex;
align-items: center;
gap: 0.75rem;
width: 100%;
gap: 0.5rem;
min-width: 0;
margin-bottom: 1rem;
min-height: 3.1rem;
border: 1px solid color-mix(in srgb, var(--color-border) 52%, transparent);
border-radius: 8px;
padding: 0.56rem 0.58rem;
background: color-mix(in srgb, var(--color-background) 50%, transparent);
color: inherit;
cursor: pointer;
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
text-align: left;
transition:
border-color 0.18s ease,
background-color 0.18s ease;
}
.discovery-feature-thumb {
flex: 0 0 auto;
width: 52px;
height: 52px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
@ -212,34 +218,15 @@
flex: 1 1 auto;
min-width: 0;
color: var(--color-text);
font-size: 0.96rem;
font-size: 0.84rem;
font-weight: 520;
line-height: 1.25;
}
.discovery-feature-desc {
margin: 1.12rem 0 0;
padding-bottom: var(--spacing-md);
color: color-mix(in srgb, var(--color-text) 72%, transparent);
font-size: 0.86rem;
line-height: 1.48;
}
.discovery-feature-card > .btn {
margin-top: auto;
min-width: 6.1rem;
min-height: 2.35rem;
border: 0;
border-radius: 8px;
color: #fff;
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
font-size: 0.88rem;
font-weight: 460;
}
.discovery-feature-card > .btn:hover,
.discovery-feature-card > .btn:focus-visible {
filter: brightness(1.06);
.discovery-feature-card:hover,
.discovery-feature-card:focus-visible {
border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
background: color-mix(in srgb, var(--color-panel) 86%, var(--color-background) 14%);
outline: none;
}
@ -272,10 +259,12 @@
height: 26px;
}
.discovery-feature-card:hover .discovery-dismiss,
.discovery-feature-card:focus-visible .discovery-dismiss,
.discovery-account-card:hover .discovery-dismiss,
.discovery-account-card:focus-within .discovery-dismiss {
.discovery-panel-dismiss {
position: static;
opacity: 0.68;
}
.discovery-account-card > .discovery-dismiss {
opacity: 0.68;
}
@ -294,32 +283,19 @@
.discovery-account-card {
position: relative;
display: grid;
gap: 1.1rem;
padding: 1.35rem 1.45rem 1.4rem;
gap: 1rem;
padding: 1.25rem 1.45rem 1.25rem;
}
.discovery-account-header {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.95rem;
align-items: center;
min-width: 0;
padding-right: 2.1rem;
}
.discovery-account-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 10px;
color: #2f6bff;
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 12%, transparent);
font-size: 1.7rem;
}
.discovery-account-copy {
display: grid;
gap: 0.3rem;
@ -357,7 +333,7 @@
min-height: 2rem;
padding: 0.32rem 0.58rem;
border: 1px solid color-mix(in srgb, var(--color-border) 52%, transparent);
border-radius: 999px;
border-radius: 8px;
background: color-mix(in srgb, var(--color-background) 50%, transparent);
color: color-mix(in srgb, var(--color-text) 74%, transparent);
font-size: 0.8rem;
@ -464,7 +440,7 @@
}
.discovery-account-cta {
justify-self: start;
justify-self: end;
display: inline-flex;
align-items: center;
justify-content: center;
@ -479,6 +455,7 @@
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
font-size: 0.88rem;
font-weight: 460;
white-space: nowrap;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
@ -536,33 +513,14 @@
}
.discovery-feature-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
display: inline-flex;
min-height: 0;
align-items: center;
}
.discovery-feature-head {
grid-column: 1;
min-width: 0;
margin-bottom: 0;
padding: 0.62rem 0.7rem;
}
.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;
width: 32px;
height: 32px;
}
.discovery-account-usage-row {
@ -577,12 +535,7 @@
}
.discovery-feature-card {
grid-template-columns: minmax(0, 1fr) auto;
padding-right: 3rem;
}
.discovery-feature-card > .btn {
justify-self: end;
padding-right: 0.7rem;
}
.discovery-account-card {
@ -594,11 +547,11 @@
padding-right: 2.2rem;
}
.discovery-account-icon {
width: 2.55rem;
height: 2.55rem;
font-size: 1.45rem;
.discovery-account-cta {
grid-column: 1 / -1;
justify-self: start;
}
}
</style>
</div>

View file

@ -64,6 +64,25 @@ const model = {
this.hasDismissedCards = true;
},
dismissFeatureCards() {
const featureIds = this.featureCards
.filter((card) => card.dismissible !== false)
.map((card) => card.id)
.filter(Boolean);
if (!featureIds.length) return;
const dismissed = this._getDismissedIds();
const dismissSet = new Set(featureIds);
for (const id of dismissSet) {
dismissed.add(id);
}
this._persistDismissedIds(dismissed);
this.cards = this.cards.filter((card) => !dismissSet.has(card.id));
this.hasDismissedCards = true;
},
undismissCards() {
localStorage.removeItem(STORAGE_KEY);
this.refreshCards();

View file

@ -7,12 +7,8 @@ class MissingApiKeyCheck(Extension):
"""Check if API keys are configured for selected model providers."""
LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
CONFIGURE_MODEL_SETTINGS_LINK = (
"""<div class="onboarding-banner-btn-container" style="margin-top: 12px;">"""
"""<button class="btn btn-ok" onclick="window.openModal('/plugins/_onboarding/webui/onboarding.html');return false;">"""
"""Start Onboarding</button>"""
"""</div>"""
)
ONBOARDING_MODAL_PATH = "/plugins/_onboarding/webui/onboarding.html"
ONBOARDING_CTA_TEXT = "Start Onboarding"
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
cfg = plugins.get_plugin_config("_model_config") or {}
@ -25,11 +21,12 @@ class MissingApiKeyCheck(Extension):
"priority": 100,
"title": "Welcome to Agent Zero!",
"html": f"""You're almost ready to chat. Please configure your models to continue.<br>
Insert your API key in the onboarding wizard.
{self.CONFIGURE_MODEL_SETTINGS_LINK}""",
Insert your API key in the onboarding wizard.""",
"cta_text": self.ONBOARDING_CTA_TEXT,
"cta_action": f"open-modal:{self.ONBOARDING_MODAL_PATH}",
"dismissible": False,
"source": "backend",
"auto_modal_path": "/plugins/_onboarding/webui/onboarding.html",
"auto_modal_path": self.ONBOARDING_MODAL_PATH,
"auto_modal_reason": "missing-api-key",
"auto_modal_priority": 100,
"auto_modal_surfaces": ["welcome", "chat-created"],
@ -69,8 +66,9 @@ class MissingApiKeyCheck(Extension):
"priority": 90,
"title": "Missing API Key for model presets",
"html": f"""No API key configured for preset models: {preset_list}.<br>
These presets will not work until you provide the required API keys.<br>
{self.CONFIGURE_MODEL_SETTINGS_LINK}""",
These presets will not work until you provide the required API keys.""",
"cta_text": self.ONBOARDING_CTA_TEXT,
"cta_action": f"open-modal:{self.ONBOARDING_MODAL_PATH}",
"dismissible": True,
"source": "backend"
})

View file

@ -82,6 +82,9 @@ async def test_missing_api_key_banner_exposes_missing_providers(monkeypatch):
)
row = next(b for b in banners if b.get("id") == "missing-api-key")
assert row.get("missing_providers") == fake
assert row["cta_text"] == "Start Onboarding"
assert row["cta_action"] == "open-modal:/plugins/_onboarding/webui/onboarding.html"
assert "onboarding-banner-btn-container" not in row["html"]
def test_model_config_frontend_tracks_inline_api_key_edits():
@ -267,6 +270,8 @@ def test_missing_api_key_banner_includes_auto_modal_metadata(monkeypatch):
assert row["type"] == "warning"
assert row["dismissible"] is False
assert row["missing_providers"] == fake
assert row["cta_text"] == "Start Onboarding"
assert row["cta_action"] == "open-modal:/plugins/_onboarding/webui/onboarding.html"
def test_provider_key_modes_for_local_and_ollama_cloud():

View file

@ -179,6 +179,8 @@ def test_oauth_discovery_card_renders_in_welcome_account_panel():
assert "discovery-oauth-accounts" in discovery_cards
assert "Your AI accounts" in discovery_cards
assert "Use your subscription-backed logins for model access." in discovery_cards
assert "Link account-backed providers such as" not in discovery_cards
assert "Connected OAuth accounts" not in discovery_cards
assert "discovery-codex-oauth" not in discovery_cards
assert "5h and weekly limits are ready." not in discovery_cards
@ -188,6 +190,9 @@ def test_oauth_discovery_card_renders_in_welcome_account_panel():
assert "account_chips" in discovery_cards
assert "discovery-account-card" in welcome_cards
assert "discovery-account-chip" in welcome_cards
assert "discovery-account-icon" not in welcome_cards
assert ".discovery-account-chip {\n display: inline-grid;" in welcome_cards
assert " border-radius: 8px;" in welcome_cards
assert "discovery-account-usage" in welcome_cards
assert "formatRemainingPercent(window)" in welcome_cards
assert "formatRemainingPercent(window)" in discovery_store

View file

@ -10,17 +10,70 @@ def _read(relative_path: str) -> str:
def test_welcome_screen_embeds_shared_new_chat_composer() -> None:
welcome = _read("webui/components/welcome/welcome-screen.html")
welcome_store = _read("webui/components/welcome/welcome-store.js")
index = _read("webui/index.html")
discovery_cards = _read(
"plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html"
)
discovery_store = _read("plugins/_discovery/webui/discovery-store.js")
assert 'class="welcome-composer"' in welcome
assert 'path="chat/attachments/inputPreview.html"' in welcome
assert 'path="chat/input/chat-bar-input.html"' in welcome
assert "x-text=\"$store.welcomeStore.heroSubtitle\"" in welcome
assert "Hello! I'm Agent Zero" in welcome
assert "'is-setup-required': $store.welcomeStore.hasBlockingSetupBanner" in welcome
assert 'class="welcome-setup-composer"' in welcome
assert "Configure your models to start chatting" in welcome
assert "Start Onboarding" in welcome
assert "openBlockingSetup()" in welcome
assert '.filter((b) => b.id !== "missing-api-key")' in welcome_store
assert "get blockingSetupBanner()" in welcome_store
assert "get heroSubtitle()" in welcome_store
assert 'return "One setup step before chatting.";' in welcome_store
assert 'return "How can I help you today?";' in welcome_store
assert "openBlockingSetup()" in welcome_store
assert '<h2>Quick Actions</h2>' in welcome
assert 'class="welcome-lower-grid"' in welcome
assert 'x-extension id="welcome-actions-end"' in welcome
assert "grid-template-columns: repeat(4, minmax(0, 1fr));" in welcome
assert '"accounts accounts channels channels"' in welcome
assert '"system system system system"' in welcome
assert "align-items: stretch;" in welcome
assert ".welcome-lower-grid .discovery-slot {\n display: contents;" in welcome
assert ".welcome-lower-grid .discovery-account-card {\n grid-area: accounts;" in welcome
assert "grid-area: system;" in welcome
assert ".welcome-composer .input-row {\n min-height: 4.9rem;" in welcome
assert "border-radius: 8px;" in welcome
assert 'aria-label="Refresh System Resources"' in welcome
assert "refreshBanners(true)" in welcome
assert 'aria-label="Dismiss System Resources"' in welcome
assert "dismissBanner($store.welcomeStore.systemResourceBanner?.id || 'system-resources')" in welcome
assert "welcome-panel-status-icon" not in welcome
assert "monitoring" not in welcome
assert ".welcome-banner {\n --welcome-banner-accent: #2f6bff;" in welcome
assert "grid-template-columns: auto minmax(0, 1fr) auto auto;" in welcome
assert ".welcome-banner.banner-warning {\n --welcome-banner-accent: #f59e0b;" in welcome
assert "border-left: 4px" not in welcome
assert 'class="welcome-banner-cta"' in welcome
assert "executeBannerAction(banner.cta_action)" in welcome
assert "executeBannerAction(action)" in welcome_store
assert 'action.startsWith("open-modal:")' in welcome_store
assert ".welcome-banner-html .onboarding-banner-btn-container" in welcome
assert "justify-content: flex-end;" in welcome
assert "justify-content: flex-start;" in welcome
assert ".welcome-banner-html .btn,\n .welcome-banner-html button" in welcome
assert "background-color: #4248f1;" in welcome
assert 'aria-label="Dismiss Connect Channels"' in discovery_cards
assert "dismissFeatureCards()" in discovery_cards
assert "dismissFeatureCards() {" in discovery_store
assert 'class="discovery-feature-card"\n type="button"' in discovery_cards
assert "`${card.cta_text || 'Connect'} ${card.title}`" in discovery_cards
assert "discovery-feature-card > .btn" not in discovery_cards
assert "discovery-feature-head" not in discovery_cards
assert "discovery-account-header" in discovery_cards
assert "discovery-account-cta" in discovery_cards
assert "discovery-account-icon" not in discovery_cards
assert "x-if=\"$store.welcomeStore && $store.welcomeStore.isVisible\"" in index
assert "x-if=\"!$store.welcomeStore || !$store.welcomeStore.isVisible\"" in index

View file

@ -15,11 +15,31 @@
<main class="welcome-shell">
<header class="welcome-hero">
<h1>Welcome back <span aria-hidden="true">&#128075;</span></h1>
<p>Agent Zero is ready to help.</p>
<h1>Hello! I'm Agent Zero <span aria-hidden="true">&#128075;</span></h1>
<p x-text="$store.welcomeStore.heroSubtitle"></p>
</header>
<section class="welcome-composer" aria-label="Start a new chat">
<section class="welcome-composer"
:class="{ 'is-setup-required': $store.welcomeStore.hasBlockingSetupBanner }"
:aria-label="$store.welcomeStore.hasBlockingSetupBanner ? 'Configure models to start chatting' : 'Start a new chat'">
<template x-if="$store.welcomeStore.hasBlockingSetupBanner">
<button type="button"
class="welcome-setup-composer"
@click="$store.welcomeStore.openBlockingSetup()"
aria-label="Start onboarding">
<span class="welcome-setup-icon" aria-hidden="true">
<span class="material-symbols-outlined">warning</span>
</span>
<span class="welcome-setup-copy">
<span class="welcome-setup-title">Configure your models to start chatting</span>
<span class="welcome-setup-detail">Insert your API key in the onboarding wizard.</span>
</span>
<span class="welcome-setup-cta">
<span>Start Onboarding</span>
<span class="material-symbols-outlined" aria-hidden="true">arrow_forward</span>
</span>
</button>
</template>
<x-component path="chat/attachments/inputPreview.html"></x-component>
<x-component path="chat/input/chat-bar-input.html"></x-component>
</section>
@ -37,6 +57,14 @@
<div class="welcome-banner-title" x-text="banner.title"></div>
<div class="welcome-banner-html" x-html="banner.html"></div>
</div>
<button class="welcome-banner-cta"
type="button"
x-show="banner.cta_text && banner.cta_action"
@click.stop="$store.welcomeStore.executeBannerAction(banner.cta_action)"
:aria-label="banner.cta_text">
<span x-text="banner.cta_text"></span>
<span class="material-symbols-outlined" aria-hidden="true">arrow_forward</span>
</button>
<button class="welcome-banner-dismiss"
type="button"
x-show="banner.dismissible !== false"
@ -131,7 +159,24 @@
x-transition.opacity>
<header class="welcome-panel-header">
<h2 x-text="$store.welcomeStore.systemResourceBanner?.title || 'System Resources'"></h2>
<span class="material-symbols-outlined">monitoring</span>
<div class="welcome-panel-actions">
<button class="welcome-panel-icon-button"
type="button"
:disabled="$store.welcomeStore.bannersLoading"
@click.stop="$store.welcomeStore.refreshBanners(true)"
title="Refresh"
aria-label="Refresh System Resources">
<span class="material-symbols-outlined">refresh</span>
</button>
<button class="welcome-panel-icon-button"
type="button"
x-show="$store.welcomeStore.systemResourceBanner?.dismissible !== false"
@click.stop="$store.welcomeStore.dismissBanner($store.welcomeStore.systemResourceBanner?.id || 'system-resources')"
title="Dismiss"
aria-label="Dismiss System Resources">
<span class="material-symbols-outlined">close</span>
</button>
</div>
</header>
<div class="welcome-system-body"
x-html="$store.welcomeStore.systemResourceBanner?.html || ''"></div>
@ -189,6 +234,7 @@
gap: 1.55rem;
width: min(100%, 1220px);
margin: 0 auto;
padding-top: 4rem;
}
.welcome-hero h1 {
@ -217,10 +263,110 @@
display: contents;
}
.welcome-composer.is-setup-required > x-component {
display: none;
}
.welcome-setup-composer {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 1rem;
width: 100%;
min-height: 4.9rem;
padding: 0.74rem 0.7rem 0.74rem 0.86rem;
border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-panel) 64%, var(--color-background) 36%);
color: inherit;
cursor: pointer;
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
text-align: left;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.035),
0 18px 46px rgba(0, 0, 0, 0.18);
transition:
border-color 0.18s ease,
background-color 0.18s ease,
box-shadow 0.18s ease;
}
.welcome-setup-composer:hover,
.welcome-setup-composer:focus-visible {
border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
background: color-mix(in srgb, var(--color-panel) 74%, var(--color-background) 26%);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.045),
0 22px 54px rgba(0, 0, 0, 0.22);
outline: none;
}
.welcome-setup-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 8px;
color: #f59e0b;
background: color-mix(in srgb, #f59e0b 9%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 12%, transparent);
}
.welcome-setup-icon .material-symbols-outlined {
font-size: 1.35rem;
}
.welcome-setup-copy {
display: grid;
gap: 0.18rem;
min-width: 0;
}
.welcome-setup-title {
color: var(--color-text);
font-size: 1rem;
font-weight: 520;
line-height: 1.25;
}
.welcome-setup-detail {
color: color-mix(in srgb, var(--color-text) 58%, transparent);
font-size: 0.92rem;
line-height: 1.35;
}
.welcome-setup-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.36rem;
min-height: 2.75rem;
border-radius: 12px;
padding: 0.62rem 0.95rem;
background-color: #4248f1;
color: #fff;
font-size: 0.92rem;
font-weight: 500;
line-height: 1;
white-space: nowrap;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: background-color 0.18s ease;
}
.welcome-setup-composer:hover .welcome-setup-cta,
.welcome-setup-composer:focus-visible .welcome-setup-cta {
background-color: #353bc5;
}
.welcome-setup-cta .material-symbols-outlined {
font-size: 1.24rem;
}
.welcome-composer .input-row {
min-height: 4.9rem;
padding: 0.58rem 0.7rem 0.58rem 0.86rem;
border-radius: 16px;
border-radius: 8px;
background: color-mix(in srgb, var(--color-panel) 64%, var(--color-background) 36%);
}
@ -261,64 +407,64 @@
}
.welcome-banner {
--welcome-banner-accent: #2f6bff;
position: relative;
display: flex;
align-items: flex-start;
gap: 0.78rem;
padding: 0.9rem 1rem;
border-radius: 12px;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: start;
gap: 1rem;
min-height: 5.45rem;
padding: 1rem 1.1rem;
border-radius: 8px;
text-align: left;
}
.welcome-banner.banner-info {
border-left: 4px solid #2f6bff;
--welcome-banner-accent: #2f6bff;
}
.welcome-banner.banner-warning {
border-left: 4px solid #f59e0b;
--welcome-banner-accent: #f59e0b;
}
.welcome-banner.banner-error {
border-left: 4px solid #ef4444;
--welcome-banner-accent: #ef4444;
}
.welcome-banner-icon {
display: flex;
width: 1.6rem;
height: 1.6rem;
display: inline-flex;
width: 2.25rem;
height: 2.25rem;
align-items: center;
justify-content: center;
color: #2f6bff;
}
.banner-warning .welcome-banner-icon {
color: #f59e0b;
}
.banner-error .welcome-banner-icon {
color: #ef4444;
border-radius: 8px;
color: var(--welcome-banner-accent);
background: color-mix(in srgb, var(--welcome-banner-accent) 9%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--welcome-banner-accent) 12%, transparent);
}
.welcome-banner-icon .material-symbols-outlined {
font-size: 1.25rem;
font-size: 1.28rem;
}
.welcome-banner-content {
flex: 1;
min-width: 0;
padding-top: 0.05rem;
}
.welcome-banner-title {
margin-bottom: 0.25rem;
margin-bottom: 0.28rem;
color: var(--color-text);
font-size: 0.92rem;
font-weight: 650;
font-size: 1rem;
font-weight: 560;
line-height: 1.25;
}
.welcome-banner-html {
color: color-mix(in srgb, var(--color-text) 68%, transparent);
font-size: 0.84rem;
max-width: 72ch;
color: color-mix(in srgb, var(--color-text) 58%, transparent);
font-size: 0.92rem;
line-height: 1.45;
}
@ -331,6 +477,55 @@
text-decoration: underline;
}
.welcome-banner-html .onboarding-banner-btn-container {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.55rem;
margin-top: 0.9rem !important;
}
.welcome-banner-cta,
.welcome-banner-html .btn,
.welcome-banner-html button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.35rem;
border: 0;
border-radius: 8px;
padding: 0.55rem 0.9rem;
background-color: #4248f1;
color: #fff;
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
font-size: 0.88rem;
font-weight: 460;
line-height: 1.2;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: background-color 0.18s ease;
}
.welcome-banner-cta {
align-self: center;
justify-self: end;
gap: 0.36rem;
white-space: nowrap;
}
.welcome-banner-cta .material-symbols-outlined {
font-size: 1.05rem;
}
.welcome-banner-cta:hover,
.welcome-banner-cta:focus-visible,
.welcome-banner-html .btn:hover,
.welcome-banner-html .btn:focus-visible,
.welcome-banner-html button:hover,
.welcome-banner-html button:focus-visible {
background-color: #353bc5;
outline: none;
}
.welcome-banner-dismiss,
.welcome-undismiss-button {
display: inline-flex;
@ -348,6 +543,7 @@
width: 2rem;
height: 2rem;
border-radius: 8px;
margin: -0.2rem -0.2rem 0 0;
}
.welcome-banner-dismiss:hover,
@ -451,18 +647,33 @@
.welcome-lower-grid {
display: grid;
grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.94fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-areas:
"accounts accounts channels channels"
"system system system system";
gap: 1rem;
align-items: stretch;
}
.welcome-lower-grid .discovery-slot {
grid-column: auto;
min-width: 0;
width: 100%;
display: contents;
}
.welcome-lower-grid .discovery-features-panel {
grid-area: channels;
min-height: 0;
}
.welcome-lower-grid .discovery-account-card {
grid-area: accounts;
}
.welcome-lower-grid .discovery-undismiss {
grid-column: 1 / -1;
}
.welcome-system-card {
grid-area: system;
min-width: 0;
border-radius: 8px;
padding: 1.6rem 1.7rem;
@ -477,10 +688,41 @@
margin-bottom: 1.5rem;
}
.welcome-panel-header .material-symbols-outlined {
.welcome-panel-actions {
display: flex;
align-items: center;
gap: 0.35rem;
flex: 0 0 auto;
}
.welcome-panel-icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 0;
border-radius: 8px;
background: transparent;
color: color-mix(in srgb, var(--color-text) 64%, transparent);
cursor: pointer;
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
}
.welcome-panel-icon-button:hover,
.welcome-panel-icon-button:focus-visible {
color: var(--color-text);
font-size: 1.35rem;
opacity: 0.86;
background: color-mix(in srgb, var(--color-border) 45%, transparent);
outline: none;
}
.welcome-panel-icon-button:disabled {
cursor: default;
opacity: 0.45;
}
.welcome-panel-icon-button .material-symbols-outlined {
font-size: 1.18rem;
}
.system-resource-body {
@ -560,6 +802,14 @@
.welcome-lower-grid {
grid-template-columns: 1fr;
grid-template-areas:
"accounts"
"channels"
"system";
}
.welcome-shell {
padding-top: 4rem;
}
}
@ -571,6 +821,7 @@
.welcome-shell {
gap: 1.2rem;
padding-top: 4rem;
}
.welcome-actions {
@ -593,6 +844,54 @@
min-height: 4.35rem;
}
.welcome-setup-composer {
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 0.8rem;
min-height: 4.35rem;
padding: 0.8rem;
}
.welcome-setup-cta {
grid-column: 2;
justify-self: start;
min-height: 2.45rem;
border-radius: 8px;
padding: 0.55rem 0.85rem;
}
.welcome-banner {
grid-template-columns: auto minmax(0, 1fr);
gap: 0.82rem;
padding: 0.95rem;
}
.welcome-banner-dismiss {
position: absolute;
top: 0.7rem;
right: 0.7rem;
margin: 0;
}
.welcome-banner-content {
padding-right: 1.9rem;
}
.welcome-banner-html {
max-width: none;
}
.welcome-banner-html .onboarding-banner-btn-container {
justify-content: flex-start;
margin-top: 0.75rem !important;
}
.welcome-banner-cta {
grid-column: 2;
justify-self: start;
margin-top: 0.25rem;
}
.welcome-system-card {
padding: 1.15rem;
}

View file

@ -143,6 +143,7 @@ const model = {
get sortedBanners() {
return [...this.banners]
.filter((b) => b.id !== "system-resources")
.filter((b) => b.id !== "missing-api-key")
.filter((b) => b.type !== "hero" && b.type !== "feature")
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
},
@ -151,6 +152,43 @@ const model = {
return this.banners.find((b) => b.id === "system-resources") || null;
},
get blockingSetupBanner() {
return this.banners.find((b) => b.id === "missing-api-key") || null;
},
get hasBlockingSetupBanner() {
return Boolean(this.blockingSetupBanner);
},
get heroSubtitle() {
if (this.hasBlockingSetupBanner) {
return "One setup step before chatting.";
}
return "How can I help you today?";
},
openBlockingSetup() {
const path =
this.blockingSetupBanner?.auto_modal_path ||
"/plugins/_onboarding/webui/onboarding.html";
window.openModal(path);
},
executeBannerAction(action) {
if (!action) return;
if (action.startsWith("open-modal:")) {
const path = action.slice("open-modal:".length);
if (path) window.openModal(path);
return;
}
if (action.startsWith("open-url:")) {
const url = action.slice("open-url:".length);
if (url) window.open(url, "_blank", "noopener,noreferrer");
}
},
/**
* Dismiss a banner by ID.
*