mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Refine onboarding model setup flow
Remove composer blocking so sends always create or enter a chat and route missing-model messages through the in-thread gate. Make Utility model selection explicit by removing same-as-main sync and defaulting to each provider's utility model when available. Compact onboarding success channel cards and update static regressions for the new onboarding/gate behavior.
This commit is contained in:
parent
8d887e6f6d
commit
caf5c2f129
9 changed files with 30 additions and 123 deletions
|
|
@ -250,11 +250,11 @@
|
|||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 142px;
|
||||
padding: 0.82rem 0.9rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.72rem 0.9rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
|
|
@ -271,7 +271,6 @@
|
|||
gap: 0.62rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
|
|
@ -308,16 +307,7 @@
|
|||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
margin: 0.42rem 0 0;
|
||||
padding-bottom: 0.6rem;
|
||||
color: var(--color-text);
|
||||
font-size: 0.77rem;
|
||||
line-height: 1.34;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
margin-top: auto;
|
||||
min-height: 36px;
|
||||
padding: 0.44rem 0.78rem;
|
||||
}
|
||||
|
|
@ -554,37 +544,6 @@
|
|||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-head {
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.discovery-feature-thumb {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.discovery-feature-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.discovery-cta-link {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -626,15 +585,6 @@
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.discovery-feature-card > .btn {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.discovery-cli-title {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ export const store = createStore("onboarding", {
|
|||
providerDetails: {},
|
||||
selectedProviderId: "",
|
||||
selectedProviderOrigin: "cloud",
|
||||
sameAsMain: true,
|
||||
userTouchedModel: {
|
||||
chat_model: false,
|
||||
utility_model: false,
|
||||
|
|
@ -128,7 +127,6 @@ export const store = createStore("onboarding", {
|
|||
this.providerDetails = {};
|
||||
this.selectedProviderId = "";
|
||||
this.selectedProviderOrigin = "cloud";
|
||||
this.sameAsMain = true;
|
||||
this.userTouchedModel = { chat_model: false, utility_model: false };
|
||||
this.modelDropdown = {
|
||||
chat_model: { models: [], open: false, loading: false, error: "", source: "" },
|
||||
|
|
@ -805,50 +803,34 @@ export const store = createStore("onboarding", {
|
|||
this.config[slotKey].name = modelName;
|
||||
this.userTouchedModel[slotKey] = true;
|
||||
this.modelDropdown[slotKey].open = false;
|
||||
if (slotKey === "chat_model" && this.sameAsMain) {
|
||||
this.syncUtilityWithMain();
|
||||
}
|
||||
},
|
||||
|
||||
markModelTouched(slotKey) {
|
||||
this.userTouchedModel[slotKey] = true;
|
||||
if (slotKey === "chat_model" && this.sameAsMain) {
|
||||
this.syncUtilityWithMain();
|
||||
}
|
||||
},
|
||||
|
||||
prepareUtilityDefaults() {
|
||||
ensureSlot(this.config, "utility_model");
|
||||
if (this.sameAsMain) {
|
||||
this.syncUtilityWithMain();
|
||||
return;
|
||||
}
|
||||
const mainProvider = this.config.chat_model.provider;
|
||||
const meta = this.providerMeta(mainProvider);
|
||||
this.applyProviderToSlot("utility_model", mainProvider, meta);
|
||||
},
|
||||
|
||||
syncUtilityWithMain() {
|
||||
ensureSlot(this.config, "utility_model");
|
||||
if (!this.sameAsMain || !this.config?.chat_model) return;
|
||||
this.config.utility_model.provider = this.config.chat_model.provider;
|
||||
this.config.utility_model.name = this.config.chat_model.name;
|
||||
this.config.utility_model.api_base = this.config.chat_model.api_base || "";
|
||||
this.config.utility_model.kwargs = clone(this.config.chat_model.kwargs || {});
|
||||
this.applyProviderToSlot("utility_model", mainProvider, this.providerMeta(mainProvider));
|
||||
if (!this.config.utility_model.api_base) {
|
||||
this.config.utility_model.api_base = this.config.chat_model.api_base || "";
|
||||
}
|
||||
},
|
||||
|
||||
async utilityProviderChanged() {
|
||||
const providerId = this.config.utility_model.provider;
|
||||
this.sameAsMain = providerId === this.config.chat_model.provider;
|
||||
this.userTouchedModel.utility_model = false;
|
||||
this.config.utility_model.api_base = "";
|
||||
this.applyProviderToSlot("utility_model", providerId, this.providerMeta(providerId));
|
||||
if (!this.config.utility_model.api_base && providerId === this.config.chat_model.provider) {
|
||||
this.config.utility_model.api_base = this.config.chat_model.api_base || "";
|
||||
}
|
||||
await this.loadModels("utility_model");
|
||||
},
|
||||
|
||||
async completeSetup() {
|
||||
this.saving = true;
|
||||
try {
|
||||
if (this.sameAsMain) this.syncUtilityWithMain();
|
||||
await modelConfigStore.persistApiKeysForConfig(this.config);
|
||||
const response = await fetchApi(`${MODEL_CONFIG_API}/model_config_set`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -821,13 +821,10 @@
|
|||
|
||||
<section x-show="$store.onboarding.isStep('utility')" x-transition.opacity>
|
||||
<div class="utility-panel field-stack">
|
||||
<label class="soft-note">
|
||||
<input type="checkbox" x-model="$store.onboarding.sameAsMain" @change="$store.onboarding.syncUtilityWithMain()">
|
||||
Use same as Main Model
|
||||
</label>
|
||||
<div class="soft-note">The utility model handles quick internal tasks — summaries, naming, memory. A small, fast, cheap model works best here.</div>
|
||||
<div class="field">
|
||||
<label for="utility-provider">Utility provider</label>
|
||||
<select id="utility-provider" x-model="$store.onboarding.config.utility_model.provider" @change="$store.onboarding.utilityProviderChanged()" :disabled="$store.onboarding.sameAsMain">
|
||||
<select id="utility-provider" x-model="$store.onboarding.config.utility_model.provider" @change="$store.onboarding.utilityProviderChanged()">
|
||||
<template x-for="provider in $store.modelConfig.getProviders('utility_model')" :key="provider.value">
|
||||
<option :value="provider.value" x-text="provider.label"></option>
|
||||
</template>
|
||||
|
|
@ -836,17 +833,16 @@
|
|||
<div class="field relative-field" @click.outside="$store.onboarding.closeModelDropdown('utility_model')">
|
||||
<label class="model-label" for="utility-model-input">Search or enter Utility Model</label>
|
||||
<div class="model-input-row">
|
||||
<input id="utility-model-input" type="text" x-model="$store.onboarding.config.utility_model.name" @input="$store.onboarding.markModelTouched('utility_model')" @focus="$store.onboarding.openModelDropdown('utility_model')" :disabled="$store.onboarding.sameAsMain" placeholder="Search or enter a model">
|
||||
<input id="utility-model-input" type="text" x-model="$store.onboarding.config.utility_model.name" @input="$store.onboarding.markModelTouched('utility_model')" @focus="$store.onboarding.openModelDropdown('utility_model')" placeholder="Search or enter a model">
|
||||
<button type="button"
|
||||
class="model-refresh-button"
|
||||
aria-label="Refresh utility model list"
|
||||
title="Refresh model list"
|
||||
x-show="!$store.onboarding.sameAsMain"
|
||||
@click="$store.onboarding.loadModels('utility_model')"
|
||||
:disabled="$store.onboarding.modelDropdown.utility_model.loading">
|
||||
<span class="material-symbols-outlined" x-text="$store.onboarding.modelDropdown.utility_model.loading ? 'progress_activity' : 'search'"></span>
|
||||
</button>
|
||||
<div class="model-dropdown" x-show="!$store.onboarding.sameAsMain && $store.onboarding.modelDropdown.utility_model.open && !$store.onboarding.modelDropdown.utility_model.loading" x-transition.opacity>
|
||||
<div class="model-dropdown" x-show="$store.onboarding.modelDropdown.utility_model.open && !$store.onboarding.modelDropdown.utility_model.loading" x-transition.opacity>
|
||||
<template x-for="model in $store.onboarding.filteredModels('utility_model')" :key="model">
|
||||
<button type="button" class="model-item" @click="$store.onboarding.selectModel('utility_model', model)" x-text="model"></button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ def test_onboarding_contains_unified_provider_step():
|
|||
assert "Main model" in html
|
||||
assert "Refresh model list" in html
|
||||
assert "Search or enter Utility Model" in html
|
||||
|
||||
# The utility model is an intentional choice: no "same as main" shortcut.
|
||||
assert "Use same as Main Model" not in html
|
||||
assert "sameAsMain" not in html + store
|
||||
assert "Advanced Settings" in html
|
||||
assert "selectedProviderName() + ' Docs'" in html
|
||||
assert "openSelectedProviderDocs" in html + store
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None:
|
|||
assert "this.choice = \"\";" in gate_store
|
||||
assert 'document.addEventListener("model-configured"' in gate_store
|
||||
assert 'document.addEventListener("model-setup-changed"' in gate_store
|
||||
assert "const currentContext = globalThis.getContext?.();" in gate_store
|
||||
assert "bypassModelGate: true" in gate_store
|
||||
assert 'openPluginConfig("_model_config", "Advanced model configuration")' in gate_store
|
||||
assert 'openPluginConfig("_oauth"' not in gate_store
|
||||
|
|
@ -143,13 +142,18 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None:
|
|||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}""" in gate_component
|
||||
assert "Connect a model to send" in input_store
|
||||
# The composer never hard-blocks sending: the in-chat gate guides users instead.
|
||||
assert "Connect a model to send" not in input_store
|
||||
assert "blocked" not in input_store
|
||||
assert "sendDisabled" not in input_store
|
||||
assert "isBlockingSend" not in gate_store
|
||||
assert "isBlockingSend" not in index_js
|
||||
|
||||
|
||||
def test_welcome_composer_does_not_overlap_idle_progress_placeholder() -> None:
|
||||
input_store = _read("webui/components/chat/input/input-store.js")
|
||||
|
||||
assert "!!chatsStore.selected &&\n ![\"all\", \"blocked\"].includes(this._getSendState())" in input_store
|
||||
assert "!!chatsStore.selected &&\n this._getSendState() !== \"all\"" in input_store
|
||||
|
||||
|
||||
def test_welcome_composer_buttons_keep_target_geometry_without_glow() -> None:
|
||||
|
|
|
|||
|
|
@ -46,8 +46,7 @@
|
|||
<div id="chat-buttons-wrapper">
|
||||
<!-- Send button -->
|
||||
<button class="chat-button" id="send-button" aria-label="Send message" @click="$store.chatInput.sendMessage()"
|
||||
:class="$store.chatInput.sendButtonClass" :title="$store.chatInput.sendButtonTitle"
|
||||
:disabled="$store.chatInput.sendDisabled">
|
||||
:class="$store.chatInput.sendButtonClass" :title="$store.chatInput.sendButtonTitle">
|
||||
<span class="material-symbols-outlined" x-text="$store.chatInput.sendButtonIcon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -388,14 +387,6 @@
|
|||
filter: none;
|
||||
}
|
||||
|
||||
#send-button.model-gate-blocked,
|
||||
#send-button.model-gate-blocked:hover {
|
||||
background-color: color-mix(in srgb, var(--color-border) 55%, transparent);
|
||||
color: color-mix(in srgb, var(--color-text) 45%, transparent);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
#send-button:active {
|
||||
background-color: #2b309c;
|
||||
transform: translateY(1px) scale(0.98);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { openLatest as openLatestSurface } from "/js/surfaces.js";
|
|||
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
|
||||
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
||||
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
||||
import { store as modelGateStore } from "/components/chat/model-gate-store.js";
|
||||
|
||||
const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g;
|
||||
const FENCE_LINE_RE = /^```([A-Za-z0-9_-]*)?$/;
|
||||
|
|
@ -84,7 +83,6 @@ const model = {
|
|||
const hasQueue = !!messageQueueStore?.hasQueue;
|
||||
const running = !!chatsStore.selectedContext?.running;
|
||||
|
||||
if (modelGateStore?.isBlockingSend) return "blocked";
|
||||
if (hasQueue && !hasInput) return "all";
|
||||
if ((running || hasQueue) && hasInput) return "queue";
|
||||
return "normal";
|
||||
|
|
@ -93,7 +91,6 @@ const model = {
|
|||
get inputPlaceholder() {
|
||||
if (!chatsStore.selected) return "Ask anything to start a new chat";
|
||||
const state = this._getSendState();
|
||||
if (state === "blocked") return "Connect a model to send";
|
||||
if (state === "all") return "Press Enter to send queued messages";
|
||||
if (this.showProgressPlaceholder) return "";
|
||||
return "Type your message here...";
|
||||
|
|
@ -102,7 +99,7 @@ const model = {
|
|||
get showProgressPlaceholder() {
|
||||
return (
|
||||
!!chatsStore.selected &&
|
||||
!["all", "blocked"].includes(this._getSendState()) &&
|
||||
this._getSendState() !== "all" &&
|
||||
!!this.progressText &&
|
||||
!this.message
|
||||
);
|
||||
|
|
@ -118,7 +115,6 @@ const model = {
|
|||
// Computed: send button icon type
|
||||
get sendButtonIcon() {
|
||||
const state = this._getSendState();
|
||||
if (state === "blocked") return "settings";
|
||||
if (state === "all") return "send_and_archive";
|
||||
if (state === "queue") return "schedule_send";
|
||||
return "arrow_forward";
|
||||
|
|
@ -127,7 +123,6 @@ const model = {
|
|||
// Computed: send button CSS class
|
||||
get sendButtonClass() {
|
||||
const state = this._getSendState();
|
||||
if (state === "blocked") return "model-gate-blocked";
|
||||
if (state === "all") return "send-queue send-all";
|
||||
if (state === "queue") return "send-queue queue";
|
||||
return "";
|
||||
|
|
@ -136,23 +131,17 @@ const model = {
|
|||
// Computed: send button title
|
||||
get sendButtonTitle() {
|
||||
const state = this._getSendState();
|
||||
if (state === "blocked") return "Connect a model to send";
|
||||
if (state === "all") return "Send all queued messages";
|
||||
if (state === "queue") return "Add to queue";
|
||||
return "Send message";
|
||||
},
|
||||
|
||||
get sendDisabled() {
|
||||
return this._getSendState() === "blocked";
|
||||
},
|
||||
|
||||
init() {
|
||||
console.log("Input store initialized");
|
||||
// Event listeners are now handled via Alpine directives in the component
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
if (this.sendDisabled) return;
|
||||
this._syncMessageFromEditor();
|
||||
|
||||
// Capture sent prompt to per-chat history (bash-style)
|
||||
|
|
|
|||
|
|
@ -21,13 +21,6 @@ export const store = createStore("modelGate", {
|
|||
dispatching: false,
|
||||
_initialized: false,
|
||||
|
||||
get isBlockingSend() {
|
||||
this.init();
|
||||
if (!this.active || this.connected) return false;
|
||||
const currentContext = globalThis.getContext?.();
|
||||
return !this.pending?.context || !currentContext || this.pending.context === currentContext;
|
||||
},
|
||||
|
||||
get introText() {
|
||||
if (this.connected) {
|
||||
return `Model connected: ${this.connectedLabel || "ready"}`;
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ let skipOneSpeech = false;
|
|||
|
||||
export async function sendMessage(options = {}) {
|
||||
try {
|
||||
if (!options.bypassModelGate && modelGateStore.isBlockingSend) return;
|
||||
|
||||
const hasProvidedMessage = Object.prototype.hasOwnProperty.call(options, "message");
|
||||
let message = String(hasProvidedMessage ? options.message : inputStore.message).trim();
|
||||
let attachmentsWithUrls = options.attachments || attachmentsStore.getAttachmentsForSending();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue