Refine welcome screen setup surfaces

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.

Alessandro committed Jun 23, 2026 at 02:21 UTC 489acfe8c614f59f79be0cf997c6faaf019b3248
10 files changed +555 -185
plugins/_discovery/extensions/python/banners/10_discovery_cards.py
+1 -1
@@ -138,7 +138,7 @@ class DiscoveryCardsExtension(Extension):
138 "type": "hero",
139 "placement": "after-features",
140 "title": "Your AI accounts",
141 - "description": "Link account-backed providers such as ChatGPT/Codex, GitHub Copilot, Gemini API, and xAI Grok."
141 + "description": "Use your subscription-backed logins for model access."
142 if not connected_count
143 else "",
144 "cta_text": "Connect accounts" if not connected_count else "Manage accounts",
plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html
+4 -4
@@ -276,8 +276,8 @@
276
277 .discovery-feature-thumb {
278 flex: 0 0 auto;
279 - width: 44px;
280 - height: 44px;
279 + width: 32px;
280 + height: 32px;
281 display: flex;
282 align-items: center;
283 justify-content: center;
@@ -570,8 +570,8 @@
570 }
571
572 .discovery-feature-thumb {
573 - width: 46px;
574 - height: 46px;
573 + width: 32px;
574 + height: 32px;
575 }
576
577 .discovery-feature-desc {
plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html
+84 -131
@@ -11,37 +11,35 @@
11
12 <section class="discovery-features-panel" x-show="$store.discoveryStore.featureCards.length > 0">
13 <header class="discovery-panel-header">
14 - <h2>Connect Channels</h2>
15 - <p>Extend Agent Zero across your favorite platforms.</p>
14 + <div class="discovery-panel-copy">
15 + <h2>Connect Channels</h2>
16 + <p>Extend Agent Zero across your favorite platforms.</p>
17 + </div>
18 + <button class="discovery-dismiss discovery-panel-dismiss"
19 + type="button"
20 + x-show="$store.discoveryStore.featureCards.some(card => card.dismissible !== false)"
21 + @click.stop="$store.discoveryStore.dismissFeatureCards()"
22 + aria-label="Dismiss Connect Channels"
23 + title="Dismiss">
24 + <span class="material-symbols-outlined">close</span>
25 + </button>
26 </header>
27
28 <div class="discovery-features">
29 <template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
20 - <article class="discovery-feature-card"
21 - role="button"
22 - tabindex="0"
23 - @click="$store.discoveryStore.executeCta(card.cta_action)"
24 - @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
25 - @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
26 -
27 - <div class="discovery-feature-head">
28 - <div class="discovery-feature-thumb">
29 - <template x-if="card.thumbnail">
30 - <img :src="card.thumbnail" :alt="card.title" loading="lazy">
31 - </template>
32 - <template x-if="!card.thumbnail && card.icon">
33 - <span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
34 - </template>
35 - </div>
36 - <h4 class="discovery-feature-title" x-text="card.title"></h4>
37 - </div>
38 -
39 - <button class="btn btn primary"
40 - type="button"
41 - @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
42 - <span x-text="card.cta_text"></span>
43 - </button>
44 - </article>
30 + <button class="discovery-feature-card"
31 + type="button"
32 + @click="$store.discoveryStore.executeCta(card.cta_action)">
33 + <span class="discovery-feature-thumb">
34 + <template x-if="card.thumbnail">
35 + <img :src="card.thumbnail" :alt="card.title" loading="lazy">
36 + </template>
37 + <template x-if="!card.thumbnail && card.icon">
38 + <span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
39 + </template>
40 + </span>
41 + <span class="discovery-feature-title" x-text="`${card.cta_text || 'Connect'} ${card.title}`"></span>
42 + </button>
43 </template>
44 </div>
45 </section>
@@ -58,11 +56,16 @@
56 </button>
57
58 <header class="discovery-account-header">
61 - <span class="material-symbols-outlined discovery-account-icon">key</span>
59 <div class="discovery-account-copy">
60 <h2 x-text="card.title"></h2>
61 <p x-show="card.description" x-text="card.description"></p>
62 </div>
63 + <button class="btn btn primary discovery-account-cta"
64 + type="button"
65 + @click="$store.discoveryStore.executeCta(card.cta_action)">
66 + <span x-text="card.cta_text"></span>
67 + <span class="material-symbols-outlined">arrow_forward</span>
68 + </button>
69 </header>
70
71 <div class="discovery-account-chips" x-show="(card.account_chips || []).length > 0">
@@ -95,12 +98,6 @@
98 </template>
99 </div>
100
98 - <button class="btn btn primary discovery-account-cta"
99 - type="button"
100 - @click="$store.discoveryStore.executeCta(card.cta_action)">
101 - <span x-text="card.cta_text"></span>
102 - <span class="material-symbols-outlined">arrow_forward</span>
103 - </button>
101 </article>
102 </template>
103
@@ -135,15 +132,23 @@
132
133 .discovery-features-panel {
134 min-height: 100%;
138 - padding: 1.7rem 1.55rem 1.55rem;
135 + padding: 1.25rem 1.45rem;
136 }
137
138 .discovery-panel-header {
139 display: grid;
143 - gap: 0.42rem;
140 + grid-template-columns: minmax(0, 1fr) auto;
141 + gap: 0.7rem;
142 + align-items: start;
143 margin-bottom: 1.28rem;
144 }
145
146 + .discovery-panel-copy {
147 + display: grid;
148 + gap: 0.42rem;
149 + min-width: 0;
150 + }
151 +
152 .discovery-panel-header h2 {
153 margin: 0;
154 color: var(--color-text);
@@ -162,31 +167,32 @@
167 .discovery-features {
168 display: grid;
169 grid-template-columns: repeat(3, minmax(0, 1fr));
165 - gap: 0.7rem;
170 + gap: 0.65rem;
171 }
172
173 .discovery-feature-card {
169 - position: relative;
170 - display: flex;
171 - flex-direction: column;
172 - align-items: flex-start;
173 - text-align: left;
174 - }
175 -
176 - .discovery-feature-head {
177 - display: flex;
178 - flex-direction: row;
174 + display: inline-flex;
175 align-items: center;
180 - gap: 0.75rem;
181 - width: 100%;
176 + gap: 0.5rem;
177 min-width: 0;
183 - margin-bottom: 1rem;
178 + min-height: 3.1rem;
179 + border: 1px solid color-mix(in srgb, var(--color-border) 52%, transparent);
180 + border-radius: 8px;
181 + padding: 0.56rem 0.58rem;
182 + background: color-mix(in srgb, var(--color-background) 50%, transparent);
183 + color: inherit;
184 + cursor: pointer;
185 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
186 + text-align: left;
187 + transition:
188 + border-color 0.18s ease,
189 + background-color 0.18s ease;
190 }
191
192 .discovery-feature-thumb {
193 flex: 0 0 auto;
188 - width: 52px;
189 - height: 52px;
194 + width: 32px;
195 + height: 32px;
196 display: flex;
197 align-items: center;
198 justify-content: center;
@@ -212,34 +218,15 @@
218 flex: 1 1 auto;
219 min-width: 0;
220 color: var(--color-text);
215 - font-size: 0.96rem;
221 + font-size: 0.84rem;
222 font-weight: 520;
223 line-height: 1.25;
224 }
225
220 - .discovery-feature-desc {
221 - margin: 1.12rem 0 0;
222 - padding-bottom: var(--spacing-md);
223 - color: color-mix(in srgb, var(--color-text) 72%, transparent);
224 - font-size: 0.86rem;
225 - line-height: 1.48;
226 - }
227 -
228 - .discovery-feature-card > .btn {
229 - margin-top: auto;
230 - min-width: 6.1rem;
231 - min-height: 2.35rem;
232 - border: 0;
233 - border-radius: 8px;
234 - color: #fff;
235 - font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
236 - font-size: 0.88rem;
237 - font-weight: 460;
238 - }
239 -
240 - .discovery-feature-card > .btn:hover,
241 - .discovery-feature-card > .btn:focus-visible {
242 - filter: brightness(1.06);
226 + .discovery-feature-card:hover,
227 + .discovery-feature-card:focus-visible {
228 + border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
229 + background: color-mix(in srgb, var(--color-panel) 86%, var(--color-background) 14%);
230 outline: none;
231 }
232
@@ -272,10 +259,12 @@
259 height: 26px;
260 }
261
275 - .discovery-feature-card:hover .discovery-dismiss,
276 - .discovery-feature-card:focus-visible .discovery-dismiss,
277 - .discovery-account-card:hover .discovery-dismiss,
278 - .discovery-account-card:focus-within .discovery-dismiss {
262 + .discovery-panel-dismiss {
263 + position: static;
264 + opacity: 0.68;
265 + }
266 +
267 + .discovery-account-card > .discovery-dismiss {
268 opacity: 0.68;
269 }
270
@@ -294,32 +283,19 @@
283 .discovery-account-card {
284 position: relative;
285 display: grid;
297 - gap: 1.1rem;
298 - padding: 1.35rem 1.45rem 1.4rem;
286 + gap: 1rem;
287 + padding: 1.25rem 1.45rem 1.25rem;
288 }
289
290 .discovery-account-header {
291 display: grid;
303 - grid-template-columns: auto minmax(0, 1fr);
292 + grid-template-columns: minmax(0, 1fr) auto;
293 gap: 0.95rem;
294 align-items: center;
295 min-width: 0;
296 padding-right: 2.1rem;
297 }
298
310 - .discovery-account-icon {
311 - display: inline-flex;
312 - align-items: center;
313 - justify-content: center;
314 - width: 3rem;
315 - height: 3rem;
316 - border-radius: 10px;
317 - color: #2f6bff;
318 - background: color-mix(in srgb, var(--color-primary) 10%, transparent);
319 - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 12%, transparent);
320 - font-size: 1.7rem;
321 - }
322 -
299 .discovery-account-copy {
300 display: grid;
301 gap: 0.3rem;
@@ -357,7 +333,7 @@
333 min-height: 2rem;
334 padding: 0.32rem 0.58rem;
335 border: 1px solid color-mix(in srgb, var(--color-border) 52%, transparent);
360 - border-radius: 999px;
336 + border-radius: 8px;
337 background: color-mix(in srgb, var(--color-background) 50%, transparent);
338 color: color-mix(in srgb, var(--color-text) 74%, transparent);
339 font-size: 0.8rem;
@@ -464,7 +440,7 @@
440 }
441
442 .discovery-account-cta {
467 - justify-self: start;
443 + justify-self: end;
444 display: inline-flex;
445 align-items: center;
446 justify-content: center;
@@ -479,6 +455,7 @@
455 font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
456 font-size: 0.88rem;
457 font-weight: 460;
458 + white-space: nowrap;
459 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
460 }
461
@@ -536,33 +513,14 @@
513 }
514
515 .discovery-feature-card {
539 - display: grid;
540 - grid-template-columns: minmax(0, 1fr) auto;
541 - gap: 0.75rem;
516 + display: inline-flex;
517 min-height: 0;
543 - align-items: center;
544 - }
545 -
546 - .discovery-feature-head {
547 - grid-column: 1;
548 - min-width: 0;
549 - margin-bottom: 0;
518 + padding: 0.62rem 0.7rem;
519 }
520
521 .discovery-feature-thumb {
553 - width: 46px;
554 - height: 46px;
555 - }
556 -
557 - .discovery-feature-desc {
558 - display: none;
559 - }
560 -
561 - .discovery-feature-card > .btn {
562 - grid-column: 2;
563 - grid-row: 1;
564 - align-self: center;
565 - margin-top: 0;
522 + width: 32px;
523 + height: 32px;
524 }
525
526 .discovery-account-usage-row {
@@ -577,12 +535,7 @@
535 }
536
537 .discovery-feature-card {
580 - grid-template-columns: minmax(0, 1fr) auto;
581 - padding-right: 3rem;
582 - }
583 -
584 - .discovery-feature-card > .btn {
585 - justify-self: end;
538 + padding-right: 0.7rem;
539 }
540
541 .discovery-account-card {
@@ -594,11 +547,11 @@
547 padding-right: 2.2rem;
548 }
549
597 - .discovery-account-icon {
598 - width: 2.55rem;
599 - height: 2.55rem;
600 - font-size: 1.45rem;
550 + .discovery-account-cta {
551 + grid-column: 1 / -1;
552 + justify-self: start;
553 }
554 +
555 }
556 </style>
557 </div>
plugins/_discovery/webui/discovery-store.js
+19
@@ -64,6 +64,25 @@ const model = {
64 this.hasDismissedCards = true;
65 },
66
67 + dismissFeatureCards() {
68 + const featureIds = this.featureCards
69 + .filter((card) => card.dismissible !== false)
70 + .map((card) => card.id)
71 + .filter(Boolean);
72 +
73 + if (!featureIds.length) return;
74 +
75 + const dismissed = this._getDismissedIds();
76 + const dismissSet = new Set(featureIds);
77 + for (const id of dismissSet) {
78 + dismissed.add(id);
79 + }
80 + this._persistDismissedIds(dismissed);
81 +
82 + this.cards = this.cards.filter((card) => !dismissSet.has(card.id));
83 + this.hasDismissedCards = true;
84 + },
85 +
86 undismissCards() {
87 localStorage.removeItem(STORAGE_KEY);
88 this.refreshCards();
plugins/_model_config/extensions/python/banners/_20_missing_api_key.py
+9 -11
@@ -7,12 +7,8 @@ class MissingApiKeyCheck(Extension):
7 """Check if API keys are configured for selected model providers."""
8
9 LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
10 - CONFIGURE_MODEL_SETTINGS_LINK = (
11 - """<div class="onboarding-banner-btn-container" style="margin-top: 12px;">"""
12 - """<button class="btn btn-ok" onclick="window.openModal('/plugins/_onboarding/webui/onboarding.html');return false;">"""
13 - """Start Onboarding</button>"""
14 - """</div>"""
15 - )
10 + ONBOARDING_MODAL_PATH = "/plugins/_onboarding/webui/onboarding.html"
11 + ONBOARDING_CTA_TEXT = "Start Onboarding"
12
13 async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
14 cfg = plugins.get_plugin_config("_model_config") or {}
@@ -25,11 +21,12 @@ class MissingApiKeyCheck(Extension):
21 "priority": 100,
22 "title": "Welcome to Agent Zero!",
23 "html": f"""You're almost ready to chat. Please configure your models to continue.<br>
28 - Insert your API key in the onboarding wizard.
29 - {self.CONFIGURE_MODEL_SETTINGS_LINK}""",
24 + Insert your API key in the onboarding wizard.""",
25 + "cta_text": self.ONBOARDING_CTA_TEXT,
26 + "cta_action": f"open-modal:{self.ONBOARDING_MODAL_PATH}",
27 "dismissible": False,
28 "source": "backend",
32 - "auto_modal_path": "/plugins/_onboarding/webui/onboarding.html",
29 + "auto_modal_path": self.ONBOARDING_MODAL_PATH,
30 "auto_modal_reason": "missing-api-key",
31 "auto_modal_priority": 100,
32 "auto_modal_surfaces": ["welcome", "chat-created"],
@@ -69,8 +66,9 @@ class MissingApiKeyCheck(Extension):
66 "priority": 90,
67 "title": "Missing API Key for model presets",
68 "html": f"""No API key configured for preset models: {preset_list}.<br>
72 - These presets will not work until you provide the required API keys.<br>
73 - {self.CONFIGURE_MODEL_SETTINGS_LINK}""",
69 + These presets will not work until you provide the required API keys.""",
70 + "cta_text": self.ONBOARDING_CTA_TEXT,
71 + "cta_action": f"open-modal:{self.ONBOARDING_MODAL_PATH}",
72 "dismissible": True,
73 "source": "backend"
74 })
tests/test_model_config_api_keys.py
+5
@@ -82,6 +82,9 @@ async def test_missing_api_key_banner_exposes_missing_providers(monkeypatch):
82 )
83 row = next(b for b in banners if b.get("id") == "missing-api-key")
84 assert row.get("missing_providers") == fake
85 + assert row["cta_text"] == "Start Onboarding"
86 + assert row["cta_action"] == "open-modal:/plugins/_onboarding/webui/onboarding.html"
87 + assert "onboarding-banner-btn-container" not in row["html"]
88
89
90 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):
270 assert row["type"] == "warning"
271 assert row["dismissible"] is False
272 assert row["missing_providers"] == fake
273 + assert row["cta_text"] == "Start Onboarding"
274 + assert row["cta_action"] == "open-modal:/plugins/_onboarding/webui/onboarding.html"
275
276
277 def test_provider_key_modes_for_local_and_ollama_cloud():
tests/test_oauth_static.py
+5
@@ -179,6 +179,8 @@ def test_oauth_discovery_card_renders_in_welcome_account_panel():
179
180 assert "discovery-oauth-accounts" in discovery_cards
181 assert "Your AI accounts" in discovery_cards
182 + assert "Use your subscription-backed logins for model access." in discovery_cards
183 + assert "Link account-backed providers such as" not in discovery_cards
184 assert "Connected OAuth accounts" not in discovery_cards
185 assert "discovery-codex-oauth" not in discovery_cards
186 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():
190 assert "account_chips" in discovery_cards
191 assert "discovery-account-card" in welcome_cards
192 assert "discovery-account-chip" in welcome_cards
193 + assert "discovery-account-icon" not in welcome_cards
194 + assert ".discovery-account-chip {\n display: inline-grid;" in welcome_cards
195 + assert " border-radius: 8px;" in welcome_cards
196 assert "discovery-account-usage" in welcome_cards
197 assert "formatRemainingPercent(window)" in welcome_cards
198 assert "formatRemainingPercent(window)" in discovery_store
tests/test_welcome_composer_static.py
+53
@@ -10,17 +10,70 @@ def _read(relative_path: str) -> str:
10
11 def test_welcome_screen_embeds_shared_new_chat_composer() -> None:
12 welcome = _read("webui/components/welcome/welcome-screen.html")
13 + welcome_store = _read("webui/components/welcome/welcome-store.js")
14 index = _read("webui/index.html")
15 discovery_cards = _read(
16 "plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html"
17 )
18 + discovery_store = _read("plugins/_discovery/webui/discovery-store.js")
19
20 assert 'class="welcome-composer"' in welcome
21 assert 'path="chat/attachments/inputPreview.html"' in welcome
22 assert 'path="chat/input/chat-bar-input.html"' in welcome
23 + assert "x-text=\"$store.welcomeStore.heroSubtitle\"" in welcome
24 + assert "Hello! I'm Agent Zero" in welcome
25 + assert "'is-setup-required': $store.welcomeStore.hasBlockingSetupBanner" in welcome
26 + assert 'class="welcome-setup-composer"' in welcome
27 + assert "Configure your models to start chatting" in welcome
28 + assert "Start Onboarding" in welcome
29 + assert "openBlockingSetup()" in welcome
30 + assert '.filter((b) => b.id !== "missing-api-key")' in welcome_store
31 + assert "get blockingSetupBanner()" in welcome_store
32 + assert "get heroSubtitle()" in welcome_store
33 + assert 'return "One setup step before chatting.";' in welcome_store
34 + assert 'return "How can I help you today?";' in welcome_store
35 + assert "openBlockingSetup()" in welcome_store
36 assert '<h2>Quick Actions</h2>' in welcome
37 assert 'class="welcome-lower-grid"' in welcome
38 assert 'x-extension id="welcome-actions-end"' in welcome
39 + assert "grid-template-columns: repeat(4, minmax(0, 1fr));" in welcome
40 + assert '"accounts accounts channels channels"' in welcome
41 + assert '"system system system system"' in welcome
42 + assert "align-items: stretch;" in welcome
43 + assert ".welcome-lower-grid .discovery-slot {\n display: contents;" in welcome
44 + assert ".welcome-lower-grid .discovery-account-card {\n grid-area: accounts;" in welcome
45 + assert "grid-area: system;" in welcome
46 + assert ".welcome-composer .input-row {\n min-height: 4.9rem;" in welcome
47 + assert "border-radius: 8px;" in welcome
48 + assert 'aria-label="Refresh System Resources"' in welcome
49 + assert "refreshBanners(true)" in welcome
50 + assert 'aria-label="Dismiss System Resources"' in welcome
51 + assert "dismissBanner($store.welcomeStore.systemResourceBanner?.id || 'system-resources')" in welcome
52 + assert "welcome-panel-status-icon" not in welcome
53 + assert "monitoring" not in welcome
54 + assert ".welcome-banner {\n --welcome-banner-accent: #2f6bff;" in welcome
55 + assert "grid-template-columns: auto minmax(0, 1fr) auto auto;" in welcome
56 + assert ".welcome-banner.banner-warning {\n --welcome-banner-accent: #f59e0b;" in welcome
57 + assert "border-left: 4px" not in welcome
58 + assert 'class="welcome-banner-cta"' in welcome
59 + assert "executeBannerAction(banner.cta_action)" in welcome
60 + assert "executeBannerAction(action)" in welcome_store
61 + assert 'action.startsWith("open-modal:")' in welcome_store
62 + assert ".welcome-banner-html .onboarding-banner-btn-container" in welcome
63 + assert "justify-content: flex-end;" in welcome
64 + assert "justify-content: flex-start;" in welcome
65 + assert ".welcome-banner-html .btn,\n .welcome-banner-html button" in welcome
66 + assert "background-color: #4248f1;" in welcome
67 + assert 'aria-label="Dismiss Connect Channels"' in discovery_cards
68 + assert "dismissFeatureCards()" in discovery_cards
69 + assert "dismissFeatureCards() {" in discovery_store
70 + assert 'class="discovery-feature-card"\n type="button"' in discovery_cards
71 + assert "`${card.cta_text || 'Connect'} ${card.title}`" in discovery_cards
72 + assert "discovery-feature-card > .btn" not in discovery_cards
73 + assert "discovery-feature-head" not in discovery_cards
74 + assert "discovery-account-header" in discovery_cards
75 + assert "discovery-account-cta" in discovery_cards
76 + assert "discovery-account-icon" not in discovery_cards
77
78 assert "x-if=\"$store.welcomeStore && $store.welcomeStore.isVisible\"" in index
79 assert "x-if=\"!$store.welcomeStore || !$store.welcomeStore.isVisible\"" in index
webui/components/welcome/welcome-screen.html
+337 -38
@@ -15,11 +15,31 @@
15
16 <main class="welcome-shell">
17 <header class="welcome-hero">
18 - <h1>Welcome back <span aria-hidden="true">&#128075;</span></h1>
19 - <p>Agent Zero is ready to help.</p>
18 + <h1>Hello! I'm Agent Zero <span aria-hidden="true">&#128075;</span></h1>
19 + <p x-text="$store.welcomeStore.heroSubtitle"></p>
20 </header>
21
22 - <section class="welcome-composer" aria-label="Start a new chat">
22 + <section class="welcome-composer"
23 + :class="{ 'is-setup-required': $store.welcomeStore.hasBlockingSetupBanner }"
24 + :aria-label="$store.welcomeStore.hasBlockingSetupBanner ? 'Configure models to start chatting' : 'Start a new chat'">
25 + <template x-if="$store.welcomeStore.hasBlockingSetupBanner">
26 + <button type="button"
27 + class="welcome-setup-composer"
28 + @click="$store.welcomeStore.openBlockingSetup()"
29 + aria-label="Start onboarding">
30 + <span class="welcome-setup-icon" aria-hidden="true">
31 + <span class="material-symbols-outlined">warning</span>
32 + </span>
33 + <span class="welcome-setup-copy">
34 + <span class="welcome-setup-title">Configure your models to start chatting</span>
35 + <span class="welcome-setup-detail">Insert your API key in the onboarding wizard.</span>
36 + </span>
37 + <span class="welcome-setup-cta">
38 + <span>Start Onboarding</span>
39 + <span class="material-symbols-outlined" aria-hidden="true">arrow_forward</span>
40 + </span>
41 + </button>
42 + </template>
43 <x-component path="chat/attachments/inputPreview.html"></x-component>
44 <x-component path="chat/input/chat-bar-input.html"></x-component>
45 </section>
@@ -37,6 +57,14 @@
57 <div class="welcome-banner-title" x-text="banner.title"></div>
58 <div class="welcome-banner-html" x-html="banner.html"></div>
59 </div>
60 + <button class="welcome-banner-cta"
61 + type="button"
62 + x-show="banner.cta_text && banner.cta_action"
63 + @click.stop="$store.welcomeStore.executeBannerAction(banner.cta_action)"
64 + :aria-label="banner.cta_text">
65 + <span x-text="banner.cta_text"></span>
66 + <span class="material-symbols-outlined" aria-hidden="true">arrow_forward</span>
67 + </button>
68 <button class="welcome-banner-dismiss"
69 type="button"
70 x-show="banner.dismissible !== false"
@@ -131,7 +159,24 @@
159 x-transition.opacity>
160 <header class="welcome-panel-header">
161 <h2 x-text="$store.welcomeStore.systemResourceBanner?.title || 'System Resources'"></h2>
134 - <span class="material-symbols-outlined">monitoring</span>
162 + <div class="welcome-panel-actions">
163 + <button class="welcome-panel-icon-button"
164 + type="button"
165 + :disabled="$store.welcomeStore.bannersLoading"
166 + @click.stop="$store.welcomeStore.refreshBanners(true)"
167 + title="Refresh"
168 + aria-label="Refresh System Resources">
169 + <span class="material-symbols-outlined">refresh</span>
170 + </button>
171 + <button class="welcome-panel-icon-button"
172 + type="button"
173 + x-show="$store.welcomeStore.systemResourceBanner?.dismissible !== false"
174 + @click.stop="$store.welcomeStore.dismissBanner($store.welcomeStore.systemResourceBanner?.id || 'system-resources')"
175 + title="Dismiss"
176 + aria-label="Dismiss System Resources">
177 + <span class="material-symbols-outlined">close</span>
178 + </button>
179 + </div>
180 </header>
181 <div class="welcome-system-body"
182 x-html="$store.welcomeStore.systemResourceBanner?.html || ''"></div>
@@ -189,6 +234,7 @@
234 gap: 1.55rem;
235 width: min(100%, 1220px);
236 margin: 0 auto;
237 + padding-top: 4rem;
238 }
239
240 .welcome-hero h1 {
@@ -217,10 +263,110 @@
263 display: contents;
264 }
265
266 + .welcome-composer.is-setup-required > x-component {
267 + display: none;
268 + }
269 +
270 + .welcome-setup-composer {
271 + display: grid;
272 + grid-template-columns: auto minmax(0, 1fr) auto;
273 + align-items: center;
274 + gap: 1rem;
275 + width: 100%;
276 + min-height: 4.9rem;
277 + padding: 0.74rem 0.7rem 0.74rem 0.86rem;
278 + border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
279 + border-radius: 8px;
280 + background: color-mix(in srgb, var(--color-panel) 64%, var(--color-background) 36%);
281 + color: inherit;
282 + cursor: pointer;
283 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
284 + text-align: left;
285 + box-shadow:
286 + inset 0 1px 0 rgba(255, 255, 255, 0.035),
287 + 0 18px 46px rgba(0, 0, 0, 0.18);
288 + transition:
289 + border-color 0.18s ease,
290 + background-color 0.18s ease,
291 + box-shadow 0.18s ease;
292 + }
293 +
294 + .welcome-setup-composer:hover,
295 + .welcome-setup-composer:focus-visible {
296 + border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
297 + background: color-mix(in srgb, var(--color-panel) 74%, var(--color-background) 26%);
298 + box-shadow:
299 + inset 0 1px 0 rgba(255, 255, 255, 0.045),
300 + 0 22px 54px rgba(0, 0, 0, 0.22);
301 + outline: none;
302 + }
303 +
304 + .welcome-setup-icon {
305 + display: inline-flex;
306 + align-items: center;
307 + justify-content: center;
308 + width: 2.5rem;
309 + height: 2.5rem;
310 + border-radius: 8px;
311 + color: #f59e0b;
312 + background: color-mix(in srgb, #f59e0b 9%, transparent);
313 + box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 12%, transparent);
314 + }
315 +
316 + .welcome-setup-icon .material-symbols-outlined {
317 + font-size: 1.35rem;
318 + }
319 +
320 + .welcome-setup-copy {
321 + display: grid;
322 + gap: 0.18rem;
323 + min-width: 0;
324 + }
325 +
326 + .welcome-setup-title {
327 + color: var(--color-text);
328 + font-size: 1rem;
329 + font-weight: 520;
330 + line-height: 1.25;
331 + }
332 +
333 + .welcome-setup-detail {
334 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
335 + font-size: 0.92rem;
336 + line-height: 1.35;
337 + }
338 +
339 + .welcome-setup-cta {
340 + display: inline-flex;
341 + align-items: center;
342 + justify-content: center;
343 + gap: 0.36rem;
344 + min-height: 2.75rem;
345 + border-radius: 12px;
346 + padding: 0.62rem 0.95rem;
347 + background-color: #4248f1;
348 + color: #fff;
349 + font-size: 0.92rem;
350 + font-weight: 500;
351 + line-height: 1;
352 + white-space: nowrap;
353 + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
354 + transition: background-color 0.18s ease;
355 + }
356 +
357 + .welcome-setup-composer:hover .welcome-setup-cta,
358 + .welcome-setup-composer:focus-visible .welcome-setup-cta {
359 + background-color: #353bc5;
360 + }
361 +
362 + .welcome-setup-cta .material-symbols-outlined {
363 + font-size: 1.24rem;
364 + }
365 +
366 .welcome-composer .input-row {
367 min-height: 4.9rem;
368 padding: 0.58rem 0.7rem 0.58rem 0.86rem;
223 - border-radius: 16px;
369 + border-radius: 8px;
370 background: color-mix(in srgb, var(--color-panel) 64%, var(--color-background) 36%);
371 }
372
@@ -261,64 +407,64 @@
407 }
408
409 .welcome-banner {
410 + --welcome-banner-accent: #2f6bff;
411 position: relative;
265 - display: flex;
266 - align-items: flex-start;
267 - gap: 0.78rem;
268 - padding: 0.9rem 1rem;
269 - border-radius: 12px;
412 + display: grid;
413 + grid-template-columns: auto minmax(0, 1fr) auto auto;
414 + align-items: start;
415 + gap: 1rem;
416 + min-height: 5.45rem;
417 + padding: 1rem 1.1rem;
418 + border-radius: 8px;
419 text-align: left;
420 }
421
422 .welcome-banner.banner-info {
274 - border-left: 4px solid #2f6bff;
423 + --welcome-banner-accent: #2f6bff;
424 }
425
426 .welcome-banner.banner-warning {
278 - border-left: 4px solid #f59e0b;
427 + --welcome-banner-accent: #f59e0b;
428 }
429
430 .welcome-banner.banner-error {
282 - border-left: 4px solid #ef4444;
431 + --welcome-banner-accent: #ef4444;
432 }
433
434 .welcome-banner-icon {
286 - display: flex;
287 - width: 1.6rem;
288 - height: 1.6rem;
435 + display: inline-flex;
436 + width: 2.25rem;
437 + height: 2.25rem;
438 align-items: center;
439 justify-content: center;
291 - color: #2f6bff;
292 - }
293 -
294 - .banner-warning .welcome-banner-icon {
295 - color: #f59e0b;
296 - }
297 -
298 - .banner-error .welcome-banner-icon {
299 - color: #ef4444;
440 + border-radius: 8px;
441 + color: var(--welcome-banner-accent);
442 + background: color-mix(in srgb, var(--welcome-banner-accent) 9%, transparent);
443 + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--welcome-banner-accent) 12%, transparent);
444 }
445
446 .welcome-banner-icon .material-symbols-outlined {
303 - font-size: 1.25rem;
447 + font-size: 1.28rem;
448 }
449
450 .welcome-banner-content {
451 flex: 1;
452 min-width: 0;
453 + padding-top: 0.05rem;
454 }
455
456 .welcome-banner-title {
312 - margin-bottom: 0.25rem;
457 + margin-bottom: 0.28rem;
458 color: var(--color-text);
314 - font-size: 0.92rem;
315 - font-weight: 650;
459 + font-size: 1rem;
460 + font-weight: 560;
461 line-height: 1.25;
462 }
463
464 .welcome-banner-html {
320 - color: color-mix(in srgb, var(--color-text) 68%, transparent);
321 - font-size: 0.84rem;
465 + max-width: 72ch;
466 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
467 + font-size: 0.92rem;
468 line-height: 1.45;
469 }
470
@@ -331,6 +477,55 @@
477 text-decoration: underline;
478 }
479
480 + .welcome-banner-html .onboarding-banner-btn-container {
481 + display: flex;
482 + flex-wrap: wrap;
483 + justify-content: flex-end;
484 + gap: 0.55rem;
485 + margin-top: 0.9rem !important;
486 + }
487 +
488 + .welcome-banner-cta,
489 + .welcome-banner-html .btn,
490 + .welcome-banner-html button {
491 + display: inline-flex;
492 + align-items: center;
493 + justify-content: center;
494 + min-height: 2.35rem;
495 + border: 0;
496 + border-radius: 8px;
497 + padding: 0.55rem 0.9rem;
498 + background-color: #4248f1;
499 + color: #fff;
500 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
501 + font-size: 0.88rem;
502 + font-weight: 460;
503 + line-height: 1.2;
504 + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
505 + transition: background-color 0.18s ease;
506 + }
507 +
508 + .welcome-banner-cta {
509 + align-self: center;
510 + justify-self: end;
511 + gap: 0.36rem;
512 + white-space: nowrap;
513 + }
514 +
515 + .welcome-banner-cta .material-symbols-outlined {
516 + font-size: 1.05rem;
517 + }
518 +
519 + .welcome-banner-cta:hover,
520 + .welcome-banner-cta:focus-visible,
521 + .welcome-banner-html .btn:hover,
522 + .welcome-banner-html .btn:focus-visible,
523 + .welcome-banner-html button:hover,
524 + .welcome-banner-html button:focus-visible {
525 + background-color: #353bc5;
526 + outline: none;
527 + }
528 +
529 .welcome-banner-dismiss,
530 .welcome-undismiss-button {
531 display: inline-flex;
@@ -348,6 +543,7 @@
543 width: 2rem;
544 height: 2rem;
545 border-radius: 8px;
546 + margin: -0.2rem -0.2rem 0 0;
547 }
548
549 .welcome-banner-dismiss:hover,
@@ -451,18 +647,33 @@
647
648 .welcome-lower-grid {
649 display: grid;
454 - grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.94fr);
650 + grid-template-columns: repeat(4, minmax(0, 1fr));
651 + grid-template-areas:
652 + "accounts accounts channels channels"
653 + "system system system system";
654 gap: 1rem;
655 align-items: stretch;
656 }
657
658 .welcome-lower-grid .discovery-slot {
460 - grid-column: auto;
461 - min-width: 0;
462 - width: 100%;
659 + display: contents;
660 + }
661 +
662 + .welcome-lower-grid .discovery-features-panel {
663 + grid-area: channels;
664 + min-height: 0;
665 + }
666 +
667 + .welcome-lower-grid .discovery-account-card {
668 + grid-area: accounts;
669 + }
670 +
671 + .welcome-lower-grid .discovery-undismiss {
672 + grid-column: 1 / -1;
673 }
674
675 .welcome-system-card {
676 + grid-area: system;
677 min-width: 0;
678 border-radius: 8px;
679 padding: 1.6rem 1.7rem;
@@ -477,10 +688,41 @@
688 margin-bottom: 1.5rem;
689 }
690
480 - .welcome-panel-header .material-symbols-outlined {
691 + .welcome-panel-actions {
692 + display: flex;
693 + align-items: center;
694 + gap: 0.35rem;
695 + flex: 0 0 auto;
696 + }
697 +
698 + .welcome-panel-icon-button {
699 + display: inline-flex;
700 + align-items: center;
701 + justify-content: center;
702 + width: 2rem;
703 + height: 2rem;
704 + border: 0;
705 + border-radius: 8px;
706 + background: transparent;
707 + color: color-mix(in srgb, var(--color-text) 64%, transparent);
708 + cursor: pointer;
709 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
710 + }
711 +
712 + .welcome-panel-icon-button:hover,
713 + .welcome-panel-icon-button:focus-visible {
714 color: var(--color-text);
482 - font-size: 1.35rem;
483 - opacity: 0.86;
715 + background: color-mix(in srgb, var(--color-border) 45%, transparent);
716 + outline: none;
717 + }
718 +
719 + .welcome-panel-icon-button:disabled {
720 + cursor: default;
721 + opacity: 0.45;
722 + }
723 +
724 + .welcome-panel-icon-button .material-symbols-outlined {
725 + font-size: 1.18rem;
726 }
727
728 .system-resource-body {
@@ -560,6 +802,14 @@
802
803 .welcome-lower-grid {
804 grid-template-columns: 1fr;
805 + grid-template-areas:
806 + "accounts"
807 + "channels"
808 + "system";
809 + }
810 +
811 + .welcome-shell {
812 + padding-top: 4rem;
813 }
814 }
815
@@ -571,6 +821,7 @@
821
822 .welcome-shell {
823 gap: 1.2rem;
824 + padding-top: 4rem;
825 }
826
827 .welcome-actions {
@@ -593,6 +844,54 @@
844 min-height: 4.35rem;
845 }
846
847 + .welcome-setup-composer {
848 + grid-template-columns: auto minmax(0, 1fr);
849 + align-items: start;
850 + gap: 0.8rem;
851 + min-height: 4.35rem;
852 + padding: 0.8rem;
853 + }
854 +
855 + .welcome-setup-cta {
856 + grid-column: 2;
857 + justify-self: start;
858 + min-height: 2.45rem;
859 + border-radius: 8px;
860 + padding: 0.55rem 0.85rem;
861 + }
862 +
863 + .welcome-banner {
864 + grid-template-columns: auto minmax(0, 1fr);
865 + gap: 0.82rem;
866 + padding: 0.95rem;
867 + }
868 +
869 + .welcome-banner-dismiss {
870 + position: absolute;
871 + top: 0.7rem;
872 + right: 0.7rem;
873 + margin: 0;
874 + }
875 +
876 + .welcome-banner-content {
877 + padding-right: 1.9rem;
878 + }
879 +
880 + .welcome-banner-html {
881 + max-width: none;
882 + }
883 +
884 + .welcome-banner-html .onboarding-banner-btn-container {
885 + justify-content: flex-start;
886 + margin-top: 0.75rem !important;
887 + }
888 +
889 + .welcome-banner-cta {
890 + grid-column: 2;
891 + justify-self: start;
892 + margin-top: 0.25rem;
893 + }
894 +
895 .welcome-system-card {
896 padding: 1.15rem;
897 }
webui/components/welcome/welcome-store.js
+38
@@ -143,6 +143,7 @@ const model = {
143 get sortedBanners() {
144 return [...this.banners]
145 .filter((b) => b.id !== "system-resources")
146 + .filter((b) => b.id !== "missing-api-key")
147 .filter((b) => b.type !== "hero" && b.type !== "feature")
148 .sort((a, b) => (b.priority || 0) - (a.priority || 0));
149 },
@@ -151,6 +152,43 @@ const model = {
152 return this.banners.find((b) => b.id === "system-resources") || null;
153 },
154
155 + get blockingSetupBanner() {
156 + return this.banners.find((b) => b.id === "missing-api-key") || null;
157 + },
158 +
159 + get hasBlockingSetupBanner() {
160 + return Boolean(this.blockingSetupBanner);
161 + },
162 +
163 + get heroSubtitle() {
164 + if (this.hasBlockingSetupBanner) {
165 + return "One setup step before chatting.";
166 + }
167 + return "How can I help you today?";
168 + },
169 +
170 + openBlockingSetup() {
171 + const path =
172 + this.blockingSetupBanner?.auto_modal_path ||
173 + "/plugins/_onboarding/webui/onboarding.html";
174 + window.openModal(path);
175 + },
176 +
177 + executeBannerAction(action) {
178 + if (!action) return;
179 +
180 + if (action.startsWith("open-modal:")) {
181 + const path = action.slice("open-modal:".length);
182 + if (path) window.openModal(path);
183 + return;
184 + }
185 +
186 + if (action.startsWith("open-url:")) {
187 + const url = action.slice("open-url:".length);
188 + if (url) window.open(url, "_blank", "noopener,noreferrer");
189 + }
190 + },
191 +
192 /**
193 * Dismiss a banner by ID.
194 *