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">👋</span></h1>
19
- <p>Agent Zero is ready to help.</p>
18
+ <h1>Hello! I'm Agent Zero <span aria-hidden="true">👋</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
*