feat(onboarding): add one-shot bootstrap and first-run setup wizard (#285)
Adds a bootstrap launcher and a blocking first-run onboarding wizard that guides new users through minimum Hermes setup from the browser UI. Supported provider flows: OpenRouter, Anthropic, OpenAI, custom OpenAI-compatible. OAuth/terminal-first flows remain via 'hermes model'. Security hardening applied during review: - /api/onboarding/setup restricted to loopback when auth disabled - Newline injection guard in _write_env_file - esc() on setup.unsupported_note in onboarding.js - Test isolation fix (send_key instead of bot_name in contamination test) - Skip markers for PyYAML-dependent tests in agent-less environments Tests: 693 passed (up from 679) Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: gabogabucho <gabogabucho@gmail.com>
This commit is contained in:
@@ -387,6 +387,7 @@ function applyBotName(){
|
||||
}
|
||||
// Pre-load workspace list so sidebar name is correct from first render
|
||||
await loadWorkspaceList();
|
||||
await loadOnboardingWizard();
|
||||
_initResizePanels();
|
||||
const saved=localStorage.getItem('hermes-webui-session');
|
||||
if(saved){
|
||||
|
||||
138
static/i18n.js
138
static/i18n.js
@@ -193,6 +193,75 @@ const LOCALES = {
|
||||
suggest_files: 'What files are in this workspace?',
|
||||
suggest_schedule: "What's on my schedule today?",
|
||||
suggest_plan: 'Help me plan a small project.',
|
||||
// onboarding
|
||||
onboarding_badge: 'FIRST RUN',
|
||||
onboarding_title: 'Welcome to Hermes Web UI',
|
||||
onboarding_lead: 'A quick guided setup will verify Hermes, save a real provider configuration, choose a workspace and model, and optionally protect the app with a password.',
|
||||
onboarding_back: 'Back',
|
||||
onboarding_continue: 'Continue',
|
||||
onboarding_open: 'Open Hermes',
|
||||
onboarding_step_system_title: 'System check',
|
||||
onboarding_step_system_desc: 'Verify Hermes Agent and config visibility.',
|
||||
onboarding_step_setup_title: 'Provider setup',
|
||||
onboarding_step_setup_desc: 'Save the minimum Hermes provider config.',
|
||||
onboarding_step_workspace_title: 'Workspace + model',
|
||||
onboarding_step_workspace_desc: 'Pick defaults for new sessions and chat.',
|
||||
onboarding_step_password_title: 'Optional password',
|
||||
onboarding_step_password_desc: 'Protect the Web UI before sharing it.',
|
||||
onboarding_step_finish_title: 'Finish',
|
||||
onboarding_step_finish_desc: 'Review and enter the app.',
|
||||
onboarding_notice_system_ready: 'Hermes Agent looks reachable from the Web UI.',
|
||||
onboarding_notice_system_unavailable: 'Hermes Agent is not fully available yet. Bootstrap can install it, but provider setup may still require a terminal.',
|
||||
onboarding_check_agent: 'Hermes Agent',
|
||||
onboarding_check_agent_ready: 'Detected and importable',
|
||||
onboarding_check_agent_missing: 'Missing or partially importable',
|
||||
onboarding_check_password: 'Password',
|
||||
onboarding_check_password_enabled: 'Already enabled',
|
||||
onboarding_check_password_disabled: 'Not enabled yet',
|
||||
onboarding_check_provider: 'Provider config',
|
||||
onboarding_check_provider_ready: 'Ready to chat',
|
||||
onboarding_check_provider_partial: 'Saved but incomplete',
|
||||
onboarding_check_provider_pending: 'Needs verification',
|
||||
onboarding_config_file: 'Config file:',
|
||||
onboarding_env_file: '.env file:',
|
||||
onboarding_unknown: 'Unknown',
|
||||
onboarding_current_provider: 'Current setup:',
|
||||
onboarding_missing_imports: 'Missing imports:',
|
||||
onboarding_notice_setup_required: 'Choose a simple provider path here. Advanced OAuth flows still belong in the Hermes CLI for now.',
|
||||
onboarding_notice_setup_already_ready: 'A working Hermes provider setup is already detected. You can keep it or replace it here.',
|
||||
onboarding_notice_workspace: 'These values reuse the same settings APIs as the normal app.',
|
||||
onboarding_workspace_label: 'Workspace',
|
||||
onboarding_workspace_or_path: 'Or enter a workspace path',
|
||||
onboarding_workspace_placeholder: '/home/you/workspace',
|
||||
onboarding_provider_label: 'Setup mode',
|
||||
onboarding_quick_setup_badge: 'quick setup',
|
||||
onboarding_api_key_label: 'API key',
|
||||
onboarding_api_key_placeholder: 'Leave blank to keep an existing saved key',
|
||||
onboarding_api_key_help_prefix: 'Saved as a secret in your Hermes .env file using',
|
||||
onboarding_base_url_label: 'Base URL',
|
||||
onboarding_base_url_placeholder: 'https://your-endpoint.example/v1',
|
||||
onboarding_base_url_help: 'Use this for OpenAI-compatible routers, self-hosted servers, LiteLLM, Ollama, LM Studio, vLLM, or similar endpoints.',
|
||||
onboarding_model_label: 'Default model',
|
||||
onboarding_workspace_help: 'Pick the model Hermes should use for new chats after setup completes.',
|
||||
onboarding_custom_model_placeholder: 'your-model-name',
|
||||
onboarding_custom_model_help: 'For custom endpoints, enter the exact model ID your server expects.',
|
||||
onboarding_notice_password_enabled: 'A password is already configured. Enter a new one only if you want to replace it.',
|
||||
onboarding_notice_password_recommended: 'Optional but recommended if you will expose the UI beyond localhost.',
|
||||
onboarding_password_label: 'Password (optional)',
|
||||
onboarding_password_placeholder: 'Leave blank to skip',
|
||||
onboarding_password_help: 'Passwords are stored through the existing settings API and hashed server-side.',
|
||||
onboarding_notice_finish: 'You can reopen Settings later to change any of this.',
|
||||
onboarding_not_set: 'Not set',
|
||||
onboarding_password_will_enable: 'Will be enabled',
|
||||
onboarding_password_skipped: 'Skipped for now',
|
||||
onboarding_finish_help: 'Finishing stores <code>onboarding_completed</code> in settings and drops you into the normal app.',
|
||||
onboarding_error_choose_workspace: 'Choose a workspace before continuing.',
|
||||
onboarding_error_choose_model: 'Choose a model before continuing.',
|
||||
onboarding_error_provider_required: 'Choose a setup mode before continuing.',
|
||||
onboarding_error_base_url_required: 'Base URL is required for custom endpoints.',
|
||||
onboarding_error_workspace_required: 'Workspace is required.',
|
||||
onboarding_error_model_required: 'Model is required.',
|
||||
onboarding_complete: 'Onboarding complete',
|
||||
},
|
||||
|
||||
es: {
|
||||
@@ -384,6 +453,75 @@ const LOCALES = {
|
||||
suggest_files: '¿Qué archivos hay en este espacio de trabajo?',
|
||||
suggest_schedule: '¿Qué tengo hoy en mi agenda?',
|
||||
suggest_plan: 'Ayúdame a planificar un proyecto pequeño.',
|
||||
// onboarding
|
||||
onboarding_badge: 'PRIMER USO',
|
||||
onboarding_title: 'Bienvenido a Hermes Web UI',
|
||||
onboarding_lead: 'Una guía rápida verificará Hermes, guardará una configuración real del proveedor, elegirá un espacio de trabajo y un modelo, y opcionalmente protegerá la app con una contraseña.',
|
||||
onboarding_back: 'Atrás',
|
||||
onboarding_continue: 'Continuar',
|
||||
onboarding_open: 'Abrir Hermes',
|
||||
onboarding_step_system_title: 'Comprobación del sistema',
|
||||
onboarding_step_system_desc: 'Verifica Hermes Agent y la visibilidad de la configuración.',
|
||||
onboarding_step_setup_title: 'Configuración del proveedor',
|
||||
onboarding_step_setup_desc: 'Guarda la configuración mínima real de Hermes.',
|
||||
onboarding_step_workspace_title: 'Espacio de trabajo + modelo',
|
||||
onboarding_step_workspace_desc: 'Elige los valores predeterminados para nuevas sesiones y chats.',
|
||||
onboarding_step_password_title: 'Contraseña opcional',
|
||||
onboarding_step_password_desc: 'Protege la Web UI antes de compartirla.',
|
||||
onboarding_step_finish_title: 'Finalizar',
|
||||
onboarding_step_finish_desc: 'Revisa todo y entra en la app.',
|
||||
onboarding_notice_system_ready: 'Parece que Hermes Agent está accesible desde la Web UI.',
|
||||
onboarding_notice_system_unavailable: 'Hermes Agent todavía no está totalmente disponible. Bootstrap puede instalarlo, pero la configuración del proveedor quizá aún requiera una terminal.',
|
||||
onboarding_check_agent: 'Hermes Agent',
|
||||
onboarding_check_agent_ready: 'Detectado e importable',
|
||||
onboarding_check_agent_missing: 'Falta o solo es parcialmente importable',
|
||||
onboarding_check_password: 'Contraseña',
|
||||
onboarding_check_password_enabled: 'Ya está activada',
|
||||
onboarding_check_password_disabled: 'Todavía no está activada',
|
||||
onboarding_check_provider: 'Configuración del proveedor',
|
||||
onboarding_check_provider_ready: 'Listo para chatear',
|
||||
onboarding_check_provider_partial: 'Guardado pero incompleto',
|
||||
onboarding_check_provider_pending: 'Necesita verificación',
|
||||
onboarding_config_file: 'Archivo de configuración:',
|
||||
onboarding_env_file: 'Archivo .env:',
|
||||
onboarding_unknown: 'Desconocido',
|
||||
onboarding_current_provider: 'Configuración actual:',
|
||||
onboarding_missing_imports: 'Importaciones faltantes:',
|
||||
onboarding_notice_setup_required: 'Elige aquí una ruta simple de proveedor. Los flujos OAuth avanzados siguen siendo del CLI de Hermes por ahora.',
|
||||
onboarding_notice_setup_already_ready: 'Ya se detectó una configuración funcional del proveedor de Hermes. Puedes conservarla o reemplazarla aquí.',
|
||||
onboarding_notice_workspace: 'Estos valores reutilizan las mismas APIs de configuración que la app normal.',
|
||||
onboarding_workspace_label: 'Espacio de trabajo',
|
||||
onboarding_workspace_or_path: 'O introduce la ruta de un espacio de trabajo',
|
||||
onboarding_workspace_placeholder: '/home/you/workspace',
|
||||
onboarding_provider_label: 'Modo de configuración',
|
||||
onboarding_quick_setup_badge: 'configuración rápida',
|
||||
onboarding_api_key_label: 'API key',
|
||||
onboarding_api_key_placeholder: 'Déjala en blanco para conservar una key ya guardada',
|
||||
onboarding_api_key_help_prefix: 'Se guarda como secreto en tu archivo .env de Hermes usando',
|
||||
onboarding_base_url_label: 'Base URL',
|
||||
onboarding_base_url_placeholder: 'https://tu-endpoint.example/v1',
|
||||
onboarding_base_url_help: 'Úsalo para routers OpenAI-compatible, servidores autoalojados, LiteLLM, Ollama, LM Studio, vLLM o endpoints parecidos.',
|
||||
onboarding_model_label: 'Modelo predeterminado',
|
||||
onboarding_workspace_help: 'Elige el modelo que Hermes debe usar para nuevos chats cuando termine la configuración.',
|
||||
onboarding_custom_model_placeholder: 'tu-modelo',
|
||||
onboarding_custom_model_help: 'Para endpoints personalizados, introduce el identificador exacto del modelo que espera tu servidor.',
|
||||
onboarding_notice_password_enabled: 'Ya hay una contraseña configurada. Introduce una nueva solo si quieres reemplazarla.',
|
||||
onboarding_notice_password_recommended: 'Es opcional, pero recomendable si vas a exponer la UI más allá de localhost.',
|
||||
onboarding_password_label: 'Contraseña (opcional)',
|
||||
onboarding_password_placeholder: 'Déjala en blanco para omitirla',
|
||||
onboarding_password_help: 'Las contraseñas se guardan mediante la API de configuración existente y se hashean en el servidor.',
|
||||
onboarding_notice_finish: 'Puedes volver a abrir Configuración más tarde para cambiar cualquiera de estos valores.',
|
||||
onboarding_not_set: 'Sin definir',
|
||||
onboarding_password_will_enable: 'Se activará',
|
||||
onboarding_password_skipped: 'Se omitirá por ahora',
|
||||
onboarding_finish_help: 'Al finalizar se guarda <code>onboarding_completed</code> en la configuración y entras en la app normal.',
|
||||
onboarding_error_choose_workspace: 'Elige un espacio de trabajo antes de continuar.',
|
||||
onboarding_error_choose_model: 'Elige un modelo antes de continuar.',
|
||||
onboarding_error_provider_required: 'Elige un modo de configuración antes de continuar.',
|
||||
onboarding_error_base_url_required: 'La base URL es obligatoria para endpoints personalizados.',
|
||||
onboarding_error_workspace_required: 'El espacio de trabajo es obligatorio.',
|
||||
onboarding_error_model_required: 'El modelo es obligatorio.',
|
||||
onboarding_complete: 'Onboarding completado',
|
||||
},
|
||||
|
||||
de: {
|
||||
|
||||
@@ -347,6 +347,26 @@
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="onboarding-overlay" id="onboardingOverlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="onboardingTitle">
|
||||
<div class="onboarding-card">
|
||||
<div class="onboarding-shell">
|
||||
<div class="onboarding-sidebar">
|
||||
<div class="onboarding-badge" data-i18n="onboarding_badge">FIRST RUN</div>
|
||||
<h2 id="onboardingTitle" data-i18n="onboarding_title">Welcome to Hermes Web UI</h2>
|
||||
<p id="onboardingLead" data-i18n="onboarding_lead">A quick guided setup will check your Hermes install, choose a workspace and model, and optionally protect the app with a password.</p>
|
||||
<div class="onboarding-steps" id="onboardingSteps"></div>
|
||||
</div>
|
||||
<div class="onboarding-main">
|
||||
<div class="onboarding-status" id="onboardingNotice"></div>
|
||||
<div class="onboarding-body" id="onboardingBody"></div>
|
||||
<div class="onboarding-actions">
|
||||
<button class="sm-btn" id="onboardingBackBtn" onclick="prevOnboardingStep()" style="display:none" data-i18n="onboarding_back">Back</button>
|
||||
<button class="sm-btn" id="onboardingNextBtn" onclick="nextOnboardingStep()" style="font-weight:700;color:var(--blue);border-color:rgba(124,185,255,.32)" data-i18n="onboarding_continue">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-overlay" id="settingsOverlay" style="display:none">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-header">
|
||||
@@ -471,6 +491,7 @@
|
||||
<script src="/static/commands.js"></script>
|
||||
<script src="/static/messages.js"></script>
|
||||
<script src="/static/panels.js"></script>
|
||||
<script src="/static/onboarding.js"></script>
|
||||
<script src="/static/boot.js"></script>
|
||||
<div class="app-dialog-overlay" id="appDialogOverlay" style="display:none" aria-hidden="true">
|
||||
<div class="app-dialog" id="appDialog" role="dialog" aria-modal="true" aria-labelledby="appDialogTitle" aria-describedby="appDialogDesc">
|
||||
|
||||
306
static/onboarding.js
Normal file
306
static/onboarding.js
Normal file
@@ -0,0 +1,306 @@
|
||||
const ONBOARDING={status:null,step:0,steps:['system','setup','workspace','password','finish'],form:{provider:'openrouter',workspace:'',model:'',password:'',apiKey:'',baseUrl:''},active:false};
|
||||
|
||||
function _getOnboardingSetupProviders(){
|
||||
return (((ONBOARDING.status||{}).setup||{}).providers)||[];
|
||||
}
|
||||
|
||||
function _getOnboardingSetupProvider(id){
|
||||
return _getOnboardingSetupProviders().find(p=>p.id===id)||null;
|
||||
}
|
||||
|
||||
function _getOnboardingCurrentSetup(){
|
||||
return (((ONBOARDING.status||{}).setup||{}).current)||{};
|
||||
}
|
||||
|
||||
function _onboardingStepMeta(key){
|
||||
return ({
|
||||
system:{title:t('onboarding_step_system_title'),desc:t('onboarding_step_system_desc')},
|
||||
setup:{title:t('onboarding_step_setup_title'),desc:t('onboarding_step_setup_desc')},
|
||||
workspace:{title:t('onboarding_step_workspace_title'),desc:t('onboarding_step_workspace_desc')},
|
||||
password:{title:t('onboarding_step_password_title'),desc:t('onboarding_step_password_desc')},
|
||||
finish:{title:t('onboarding_step_finish_title'),desc:t('onboarding_step_finish_desc')}
|
||||
})[key];
|
||||
}
|
||||
|
||||
function _renderOnboardingSteps(){
|
||||
const wrap=$('onboardingSteps');
|
||||
if(!wrap)return;
|
||||
wrap.innerHTML='';
|
||||
ONBOARDING.steps.forEach((key,idx)=>{
|
||||
const meta=_onboardingStepMeta(key);
|
||||
const item=document.createElement('div');
|
||||
item.className='onboarding-step'+(idx===ONBOARDING.step?' active':idx<ONBOARDING.step?' done':'');
|
||||
item.innerHTML=`<div class="onboarding-step-index">${idx+1}</div><div><div class="onboarding-step-title">${meta.title}</div><div class="onboarding-step-desc">${meta.desc}</div></div>`;
|
||||
wrap.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function _setOnboardingNotice(msg,kind='info'){
|
||||
const el=$('onboardingNotice');
|
||||
if(!el)return;
|
||||
if(!msg){el.style.display='none';el.textContent='';el.className='onboarding-status';return;}
|
||||
el.style.display='block';
|
||||
el.className='onboarding-status '+kind;
|
||||
el.textContent=msg;
|
||||
}
|
||||
|
||||
function _getOnboardingWorkspaceChoices(){
|
||||
const items=((ONBOARDING.status||{}).workspaces||{}).items||[];
|
||||
return items.length?items:[{name:'Home',path:ONBOARDING.form.workspace||''}];
|
||||
}
|
||||
|
||||
function _getOnboardingProviderModelChoices(){
|
||||
const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
|
||||
return provider?(provider.models||[]):[];
|
||||
}
|
||||
|
||||
function _getOnboardingSelectedModel(){
|
||||
return ONBOARDING.form.model||'';
|
||||
}
|
||||
|
||||
function _renderOnboardingModelField(){
|
||||
const choices=_getOnboardingProviderModelChoices();
|
||||
if(ONBOARDING.form.provider==='custom'){
|
||||
return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><input id="onboardingModelInput" value="${esc(_getOnboardingSelectedModel())}" placeholder="${t('onboarding_custom_model_placeholder')}" oninput="ONBOARDING.form.model=this.value"></label><p class="onboarding-copy">${t('onboarding_custom_model_help')}</p>`;
|
||||
}
|
||||
const options=choices.map(m=>`<option value="${esc(m.id)}">${esc(m.label)}</option>`).join('');
|
||||
return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><select id="onboardingModelSelect" onchange="ONBOARDING.form.model=this.value">${options}</select></label><p class="onboarding-copy">${t('onboarding_workspace_help')}</p>`;
|
||||
}
|
||||
|
||||
function _providerStatusLabel(system){
|
||||
if(system.chat_ready) return t('onboarding_check_provider_ready');
|
||||
if(system.provider_configured) return t('onboarding_check_provider_partial');
|
||||
return t('onboarding_check_provider_pending');
|
||||
}
|
||||
|
||||
function _renderOnboardingBody(){
|
||||
const body=$('onboardingBody');
|
||||
if(!body||!ONBOARDING.status)return;
|
||||
const key=ONBOARDING.steps[ONBOARDING.step];
|
||||
const system=ONBOARDING.status.system||{};
|
||||
const settings=ONBOARDING.status.settings||{};
|
||||
const setup=ONBOARDING.status.setup||{};
|
||||
const nextBtn=$('onboardingNextBtn');
|
||||
const backBtn=$('onboardingBackBtn');
|
||||
if(backBtn) backBtn.style.display=ONBOARDING.step>0?'':'none';
|
||||
if(nextBtn) nextBtn.textContent=key==='finish'?t('onboarding_open'):t('onboarding_continue');
|
||||
|
||||
if(key==='system'){
|
||||
const hermesOk=system.hermes_found&&system.imports_ok;
|
||||
const setupOk=!!system.chat_ready;
|
||||
_setOnboardingNotice(system.provider_note|| (setupOk?t('onboarding_notice_system_ready'):t('onboarding_notice_system_unavailable')),setupOk?'success':(hermesOk?'info':'warn'));
|
||||
body.innerHTML=`
|
||||
<div class="onboarding-panel-grid">
|
||||
<div class="onboarding-check ${hermesOk?'ok':'warn'}"><strong>${t('onboarding_check_agent')}</strong><span>${hermesOk?t('onboarding_check_agent_ready'):t('onboarding_check_agent_missing')}</span></div>
|
||||
<div class="onboarding-check ${(setupOk?'ok':system.provider_configured?'warn':'muted')}"><strong>${t('onboarding_check_provider')}</strong><span>${_providerStatusLabel(system)}</span></div>
|
||||
<div class="onboarding-check ${(settings.password_enabled?'ok':'muted')}"><strong>${t('onboarding_check_password')}</strong><span>${settings.password_enabled?t('onboarding_check_password_enabled'):t('onboarding_check_password_disabled')}</span></div>
|
||||
</div>
|
||||
<div class="onboarding-copy">
|
||||
<p><strong>${t('onboarding_config_file')}</strong> ${esc(system.config_path||t('onboarding_unknown'))}</p>
|
||||
<p><strong>${t('onboarding_env_file')}</strong> ${esc(system.env_path||t('onboarding_unknown'))}</p>
|
||||
<p>${esc(system.provider_note||'')}</p>
|
||||
${system.current_provider?`<p><strong>${t('onboarding_current_provider')}</strong> ${esc(system.current_provider)}${system.current_model?` — ${esc(system.current_model)}`:''}</p>`:''}
|
||||
${system.current_base_url?`<p><strong>${t('onboarding_base_url_label')}</strong> ${esc(system.current_base_url)}</p>`:''}
|
||||
${system.missing_modules&&system.missing_modules.length?`<p><strong>${t('onboarding_missing_imports')}</strong> ${esc(system.missing_modules.join(', '))}</p>`:''}
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if(key==='setup'){
|
||||
const providers=_getOnboardingSetupProviders();
|
||||
const options=providers.map(p=>`<option value="${esc(p.id)}">${esc(p.label)}${p.quick?' — '+esc(t('onboarding_quick_setup_badge')):''}</option>`).join('');
|
||||
const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider)||providers[0]||null;
|
||||
const showBaseUrl=provider&&provider.requires_base_url;
|
||||
const keyHelp=provider?`${t('onboarding_api_key_help_prefix')} ${esc(provider.env_var)}.`:'';
|
||||
_setOnboardingNotice(system.chat_ready?t('onboarding_notice_setup_already_ready'):t('onboarding_notice_setup_required'),system.chat_ready?'success':'info');
|
||||
body.innerHTML=`
|
||||
<label class="onboarding-field">
|
||||
<span>${t('onboarding_provider_label')}</span>
|
||||
<select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${options}</select>
|
||||
</label>
|
||||
<label class="onboarding-field">
|
||||
<span>${t('onboarding_api_key_label')}</span>
|
||||
<input id="onboardingApiKeyInput" type="password" value="${esc(ONBOARDING.form.apiKey||'')}" placeholder="${t('onboarding_api_key_placeholder')}" oninput="ONBOARDING.form.apiKey=this.value">
|
||||
</label>
|
||||
${showBaseUrl?`<label class="onboarding-field"><span>${t('onboarding_base_url_label')}</span><input id="onboardingBaseUrlInput" value="${esc(ONBOARDING.form.baseUrl||'')}" placeholder="${t('onboarding_base_url_placeholder')}" oninput="ONBOARDING.form.baseUrl=this.value"></label>`:''}
|
||||
<p class="onboarding-copy">${keyHelp}</p>
|
||||
${showBaseUrl?`<p class="onboarding-copy">${t('onboarding_base_url_help')}</p>`:''}
|
||||
<p class="onboarding-copy">${esc(setup.unsupported_note||'')||''}</p>`;
|
||||
const providerSel=$('onboardingProviderSelect');
|
||||
if(providerSel) providerSel.value=ONBOARDING.form.provider;
|
||||
return;
|
||||
}
|
||||
|
||||
if(key==='workspace'){
|
||||
const workspaceOptions=_getOnboardingWorkspaceChoices().map(ws=>`<option value="${esc(ws.path)}">${esc(ws.name||ws.path)} — ${esc(ws.path)}</option>`).join('');
|
||||
_setOnboardingNotice(t('onboarding_notice_workspace'), 'info');
|
||||
body.innerHTML=`
|
||||
<label class="onboarding-field">
|
||||
<span>${t('onboarding_workspace_label')}</span>
|
||||
<select id="onboardingWorkspaceSelect" onchange="syncOnboardingWorkspaceSelect(this.value)">${workspaceOptions}</select>
|
||||
</label>
|
||||
<label class="onboarding-field">
|
||||
<span>${t('onboarding_workspace_or_path')}</span>
|
||||
<input id="onboardingWorkspaceInput" value="${esc(ONBOARDING.form.workspace||'')}" placeholder="${t('onboarding_workspace_placeholder')}" oninput="ONBOARDING.form.workspace=this.value">
|
||||
</label>
|
||||
${_renderOnboardingModelField()}`;
|
||||
const wsSel=$('onboardingWorkspaceSelect');
|
||||
if(wsSel && ONBOARDING.form.workspace) wsSel.value=ONBOARDING.form.workspace;
|
||||
const modelSel=$('onboardingModelSelect');
|
||||
if(modelSel && ONBOARDING.form.model) modelSel.value=ONBOARDING.form.model;
|
||||
return;
|
||||
}
|
||||
|
||||
if(key==='password'){
|
||||
_setOnboardingNotice(settings.password_enabled?t('onboarding_notice_password_enabled'):t('onboarding_notice_password_recommended'), settings.password_enabled?'success':'info');
|
||||
body.innerHTML=`
|
||||
<label class="onboarding-field">
|
||||
<span>${t('onboarding_password_label')}</span>
|
||||
<input id="onboardingPasswordInput" type="password" value="${esc(ONBOARDING.form.password||'')}" placeholder="${t('onboarding_password_placeholder')}" oninput="ONBOARDING.form.password=this.value">
|
||||
</label>
|
||||
<p class="onboarding-copy">${t('onboarding_password_help')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
|
||||
_setOnboardingNotice(t('onboarding_notice_finish'), 'success');
|
||||
body.innerHTML=`
|
||||
<div class="onboarding-summary">
|
||||
<div><strong>${t('onboarding_provider_label')}</strong><span>${esc((provider&&provider.label)||ONBOARDING.form.provider||t('onboarding_not_set'))}</span></div>
|
||||
<div><strong>${t('onboarding_model_label')}</strong><span>${esc(_getOnboardingSelectedModel()||t('onboarding_not_set'))}</span></div>
|
||||
<div><strong>${t('onboarding_workspace_label')}</strong><span>${esc(ONBOARDING.form.workspace||t('onboarding_not_set'))}</span></div>
|
||||
<div><strong>${t('onboarding_check_password')}</strong><span>${ONBOARDING.form.password?t('onboarding_password_will_enable'):t('onboarding_password_skipped')}</span></div>
|
||||
</div>
|
||||
${ONBOARDING.form.baseUrl?`<p class="onboarding-copy"><strong>${t('onboarding_base_url_label')}</strong> ${esc(ONBOARDING.form.baseUrl)}</p>`:''}
|
||||
<p class="onboarding-copy">${t('onboarding_finish_help')}</p>`;
|
||||
}
|
||||
|
||||
function syncOnboardingWorkspaceSelect(value){
|
||||
ONBOARDING.form.workspace=value;
|
||||
const input=$('onboardingWorkspaceInput');
|
||||
if(input) input.value=value;
|
||||
}
|
||||
|
||||
function syncOnboardingProvider(value){
|
||||
const provider=_getOnboardingSetupProvider(value);
|
||||
ONBOARDING.form.provider=value;
|
||||
if(provider){
|
||||
if(!ONBOARDING.form.model || !_getOnboardingProviderModelChoices().some(m=>m.id===ONBOARDING.form.model) || value==='custom'){
|
||||
ONBOARDING.form.model=provider.default_model||'';
|
||||
}
|
||||
if(provider.requires_base_url){
|
||||
ONBOARDING.form.baseUrl=ONBOARDING.form.baseUrl||provider.default_base_url||'';
|
||||
}else{
|
||||
ONBOARDING.form.baseUrl=provider.default_base_url||'';
|
||||
}
|
||||
}
|
||||
_renderOnboardingBody();
|
||||
}
|
||||
|
||||
async function loadOnboardingWizard(){
|
||||
try{
|
||||
const status=await api('/api/onboarding/status');
|
||||
ONBOARDING.status=status;
|
||||
const current=((status.setup||{}).current)||{};
|
||||
ONBOARDING.form.provider=current.provider||'openrouter';
|
||||
ONBOARDING.form.workspace=(status.workspaces&&status.workspaces.last)||status.settings.default_workspace||'';
|
||||
ONBOARDING.form.model=status.settings.default_model||current.model||'openai/gpt-5.4-mini';
|
||||
ONBOARDING.form.password='';
|
||||
ONBOARDING.form.apiKey='';
|
||||
ONBOARDING.form.baseUrl=current.base_url||'';
|
||||
ONBOARDING.active=!status.completed;
|
||||
if(!ONBOARDING.active) return false;
|
||||
$('onboardingOverlay').style.display='flex';
|
||||
_renderOnboardingSteps();
|
||||
_renderOnboardingBody();
|
||||
return true;
|
||||
}catch(e){
|
||||
console.warn('onboarding status failed',e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function prevOnboardingStep(){
|
||||
if(ONBOARDING.step===0)return;
|
||||
ONBOARDING.step--;
|
||||
_renderOnboardingSteps();
|
||||
_renderOnboardingBody();
|
||||
}
|
||||
|
||||
async function _saveOnboardingProviderSetup(){
|
||||
const provider=(ONBOARDING.form.provider||'').trim();
|
||||
const model=(ONBOARDING.form.model||'').trim();
|
||||
const apiKey=(ONBOARDING.form.apiKey||'').trim();
|
||||
const baseUrl=(ONBOARDING.form.baseUrl||'').trim();
|
||||
const current=_getOnboardingCurrentSetup();
|
||||
const isUnchanged=current.provider===provider&&((current.model||'')===model)&&((current.base_url||'')===baseUrl);
|
||||
if(isUnchanged && !apiKey && (ONBOARDING.status.system||{}).chat_ready) return;
|
||||
const body={provider,model};
|
||||
if(apiKey) body.api_key=apiKey;
|
||||
if(baseUrl) body.base_url=baseUrl;
|
||||
const status=await api('/api/onboarding/setup',{method:'POST',body:JSON.stringify(body)});
|
||||
ONBOARDING.status=status;
|
||||
}
|
||||
|
||||
async function _saveOnboardingDefaults(){
|
||||
const workspace=(ONBOARDING.form.workspace||'').trim();
|
||||
const model=(ONBOARDING.form.model||'').trim();
|
||||
const password=(ONBOARDING.form.password||'').trim();
|
||||
if(!workspace) throw new Error(t('onboarding_error_choose_workspace'));
|
||||
if(!model) throw new Error(t('onboarding_error_choose_model'));
|
||||
const known=_getOnboardingWorkspaceChoices().some(ws=>ws.path===workspace);
|
||||
if(!known){
|
||||
await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path:workspace})});
|
||||
}
|
||||
const body={default_workspace:workspace,default_model:model};
|
||||
if(password) body._set_password=password;
|
||||
await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
||||
localStorage.setItem('hermes-webui-model',model);
|
||||
if($('modelSelect')) _applyModelToDropdown(model,$('modelSelect'));
|
||||
}
|
||||
|
||||
async function _finishOnboarding(){
|
||||
await _saveOnboardingProviderSetup();
|
||||
await _saveOnboardingDefaults();
|
||||
const done=await api('/api/onboarding/complete',{method:'POST',body:'{}'});
|
||||
ONBOARDING.status=done;
|
||||
ONBOARDING.active=false;
|
||||
$('onboardingOverlay').style.display='none';
|
||||
showToast(t('onboarding_complete'));
|
||||
await loadWorkspaceList();
|
||||
if(typeof renderSessionList==='function') await renderSessionList();
|
||||
if(!S.session && typeof newSession==='function'){
|
||||
await newSession(true);
|
||||
await renderSessionList();
|
||||
}
|
||||
}
|
||||
|
||||
async function nextOnboardingStep(){
|
||||
try{
|
||||
if(ONBOARDING.steps[ONBOARDING.step]==='setup'){
|
||||
ONBOARDING.form.provider=(($('onboardingProviderSelect')||{}).value||ONBOARDING.form.provider||'').trim();
|
||||
ONBOARDING.form.apiKey=(($('onboardingApiKeyInput')||{}).value||'').trim();
|
||||
ONBOARDING.form.baseUrl=(($('onboardingBaseUrlInput')||{}).value||ONBOARDING.form.baseUrl||'').trim();
|
||||
if(!ONBOARDING.form.provider) throw new Error(t('onboarding_error_provider_required'));
|
||||
if(ONBOARDING.form.provider==='custom' && !ONBOARDING.form.baseUrl) throw new Error(t('onboarding_error_base_url_required'));
|
||||
}
|
||||
if(ONBOARDING.steps[ONBOARDING.step]==='workspace'){
|
||||
ONBOARDING.form.workspace=(($('onboardingWorkspaceInput')||{}).value||ONBOARDING.form.workspace||'').trim();
|
||||
ONBOARDING.form.model=(($('onboardingModelInput')||{}).value||($('onboardingModelSelect')||{}).value||ONBOARDING.form.model||'').trim();
|
||||
if(!ONBOARDING.form.workspace) throw new Error(t('onboarding_error_workspace_required'));
|
||||
if(!ONBOARDING.form.model) throw new Error(t('onboarding_error_model_required'));
|
||||
}
|
||||
if(ONBOARDING.steps[ONBOARDING.step]==='password'){
|
||||
ONBOARDING.form.password=(($('onboardingPasswordInput')||{}).value||'').trim();
|
||||
}
|
||||
if(ONBOARDING.step===ONBOARDING.steps.length-1){
|
||||
await _finishOnboarding();
|
||||
return;
|
||||
}
|
||||
ONBOARDING.step++;
|
||||
_renderOnboardingSteps();
|
||||
_renderOnboardingBody();
|
||||
}catch(e){
|
||||
_setOnboardingNotice(e.message||String(e),'warn');
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,44 @@
|
||||
.app-dialog-btn:focus-visible,.app-dialog-close:focus-visible{outline:2px solid rgba(124,185,255,.85);outline-offset:2px;}
|
||||
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--surface);backdrop-filter:blur(12px);border:1px solid rgba(124,185,255,0.25);color:var(--text);font-size:13px;padding:10px 20px;border-radius:12px;pointer-events:none;opacity:0;transition:opacity .2s,transform .2s;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.3);letter-spacing:.01em;}
|
||||
.toast.show{opacity:1;transform:translateX(-50%) translateY(-2px);}
|
||||
.onboarding-overlay{position:fixed;inset:0;z-index:1050;background:rgba(7,12,19,.78);backdrop-filter:blur(8px);display:none;align-items:center;justify-content:center;padding:24px;}
|
||||
.onboarding-card{width:min(980px,100%);max-height:min(760px,94vh);overflow:auto;border:1px solid rgba(124,185,255,.16);border-radius:24px;background:linear-gradient(180deg,rgba(20,30,44,.98),rgba(11,17,27,.98));box-shadow:0 24px 80px rgba(0,0,0,.45);}
|
||||
.onboarding-shell{display:grid;grid-template-columns:minmax(240px,300px) minmax(0,1fr);}
|
||||
.onboarding-sidebar{padding:28px 24px;border-right:1px solid var(--border);background:linear-gradient(180deg,rgba(124,185,255,.08),rgba(124,185,255,.02));}
|
||||
.onboarding-sidebar h2{font-size:26px;line-height:1.15;margin-top:10px;margin-bottom:12px;letter-spacing:-.03em;}
|
||||
.onboarding-badge{display:inline-flex;padding:4px 10px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.12em;background:rgba(124,185,255,.14);color:var(--blue);}
|
||||
.onboarding-sidebar p{font-size:13px;color:var(--muted);line-height:1.7;}
|
||||
.onboarding-steps{display:flex;flex-direction:column;gap:10px;margin-top:24px;}
|
||||
.onboarding-step{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;border-radius:14px;border:1px solid transparent;background:rgba(255,255,255,.02);}
|
||||
.onboarding-step.active{border-color:rgba(124,185,255,.25);background:rgba(124,185,255,.08);}
|
||||
.onboarding-step.done{background:rgba(201,168,76,.08);}
|
||||
.onboarding-step-index{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:rgba(255,255,255,.08);color:var(--text);flex-shrink:0;}
|
||||
.onboarding-step.done .onboarding-step-index{background:rgba(201,168,76,.16);color:var(--gold);}
|
||||
.onboarding-step.active .onboarding-step-index{background:rgba(124,185,255,.18);color:var(--blue);}
|
||||
.onboarding-step-title{font-size:13px;font-weight:700;color:var(--text);}
|
||||
.onboarding-step-desc{font-size:11px;color:var(--muted);margin-top:2px;line-height:1.5;}
|
||||
.onboarding-main{padding:28px 28px 24px;display:flex;flex-direction:column;gap:18px;min-width:0;}
|
||||
.onboarding-status{display:none;padding:12px 14px;border-radius:12px;font-size:13px;line-height:1.6;border:1px solid var(--border2);background:rgba(255,255,255,.04);}
|
||||
.onboarding-status.info{color:var(--text);}
|
||||
.onboarding-status.success{color:var(--blue);border-color:rgba(124,185,255,.3);background:rgba(124,185,255,.08);}
|
||||
.onboarding-status.warn{color:var(--gold);border-color:rgba(201,168,76,.28);background:rgba(201,168,76,.08);}
|
||||
.onboarding-body{display:flex;flex-direction:column;gap:16px;}
|
||||
.onboarding-panel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;}
|
||||
.onboarding-check{padding:14px;border-radius:14px;border:1px solid var(--border);background:rgba(255,255,255,.03);display:flex;flex-direction:column;gap:5px;}
|
||||
.onboarding-check strong{font-size:13px;color:var(--text);}
|
||||
.onboarding-check span{font-size:12px;color:var(--muted);line-height:1.5;}
|
||||
.onboarding-check.ok{border-color:rgba(124,185,255,.28);background:rgba(124,185,255,.08);}
|
||||
.onboarding-check.warn{border-color:rgba(201,168,76,.25);background:rgba(201,168,76,.08);}
|
||||
.onboarding-field{display:flex;flex-direction:column;gap:6px;}
|
||||
.onboarding-field span{font-size:12px;font-weight:700;color:var(--text);}
|
||||
.onboarding-field input,.onboarding-field select{margin-bottom:0;padding:10px 12px;border-radius:10px;font-size:13px;background:var(--input-bg);border:1px solid var(--border2);color:var(--text);}
|
||||
.onboarding-copy{font-size:12px;color:var(--muted);line-height:1.7;}
|
||||
.onboarding-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;}
|
||||
.onboarding-summary div{padding:14px;border-radius:14px;background:rgba(255,255,255,.03);border:1px solid var(--border);display:flex;flex-direction:column;gap:5px;}
|
||||
.onboarding-summary strong{font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);}
|
||||
.onboarding-summary span{font-size:13px;color:var(--text);word-break:break-word;}
|
||||
.onboarding-actions{display:flex;justify-content:space-between;gap:10px;margin-top:auto;}
|
||||
.onboarding-actions .sm-btn{padding:10px 16px;}
|
||||
.reconnect-banner{display:none;background:var(--surface);border:1px solid rgba(201,168,76,0.4);border-radius:10px;padding:10px 16px;margin:10px auto;max-width:780px;font-size:13px;color:var(--gold);display:none;align-items:center;justify-content:space-between;gap:12px;}
|
||||
.reconnect-banner.visible{display:flex;}
|
||||
.reconnect-btn{padding:5px 12px;border-radius:7px;font-size:12px;font-weight:600;background:rgba(201,168,76,0.15);border:1px solid rgba(201,168,76,0.4);color:var(--gold);cursor:pointer;}
|
||||
@@ -508,6 +546,12 @@
|
||||
.tool-card{margin-left:0!important;font-size:12px;}
|
||||
/* Settings modal */
|
||||
.settings-panel{width:95vw;max-width:95vw;min-height:min(580px,88vh);max-height:92vh;}
|
||||
.onboarding-overlay{padding:12px;}
|
||||
.onboarding-shell{grid-template-columns:1fr;}
|
||||
.onboarding-sidebar{border-right:none;border-bottom:1px solid var(--border);padding:22px 18px;}
|
||||
.onboarding-main{padding:20px 18px 18px;}
|
||||
.onboarding-actions{flex-direction:column-reverse;}
|
||||
.onboarding-actions .sm-btn{width:100%;min-height:44px;}
|
||||
/* Login page responsive */
|
||||
.card{width:90vw;max-width:320px;padding:28px 24px;}
|
||||
/* Workspace panel mobile close button */
|
||||
|
||||
Reference in New Issue
Block a user