FIX: QVAC (QuantumVerse Automatic Computer) Onboarding
This commit is contained in:
+202
-71
@@ -130,9 +130,79 @@ export function createQvacView(opts) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Finish onboarding in tools-only mode (no model download). */
|
||||
function finishToolsOnly(note = '') {
|
||||
persist({
|
||||
qvacOnboarded: true,
|
||||
qvacProfile: selectedProfile || settings().qvacProfile || 'recommended',
|
||||
qvacMode: 'fallback',
|
||||
})
|
||||
setStatus('Ready (tools-only)', 'ok')
|
||||
syncModelChip()
|
||||
showPane()
|
||||
renderSamples()
|
||||
if (!messages.length) {
|
||||
appendMsg(
|
||||
'assistant',
|
||||
note ||
|
||||
'Tools-only mode is ready. Ask about host health, charts, or anomalies — answers use live agent RPCs.\n\nInstall `@qvac/sdk` and re-run setup for full local Qwen chat.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function paintSystemCheck(card, env) {
|
||||
const body = card.querySelector('.qvac-step-body')
|
||||
if (!body) return
|
||||
const ramGb = env.totalRamBytes ? (env.totalRamBytes / 1e9).toFixed(1) : '?'
|
||||
if (env.totalRamBytes) {
|
||||
selectedProfile = settings().qvacProfile || suggestProfile({ totalRamBytes: env.totalRamBytes })
|
||||
}
|
||||
const sdkLine = env.checking
|
||||
? '<li class="warn">QVAC SDK: checking…</li>'
|
||||
: `<li class="${env.sdkAvailable ? 'ok' : 'warn'}">QVAC SDK: ${
|
||||
env.sdkAvailable ? 'available' : 'not installed — tools-only mode'
|
||||
}</li>`
|
||||
body.innerHTML = `
|
||||
<ul class="qvac-check-list">
|
||||
${sdkLine}
|
||||
<li class="ok">Platform: ${escapeHtml(env.platform || 'unknown')} / ${escapeHtml(env.arch || 'unknown')}</li>
|
||||
<li class="ok">RAM (est.): ${escapeHtml(String(ramGb))} GB</li>
|
||||
${env.sdkError && !env.checking ? `<li class="warn">SDK note: ${escapeHtml(env.sdkError)}</li>` : ''}
|
||||
</ul>
|
||||
<p class="muted">Suggested profile: <strong>${escapeHtml(getProfile(selectedProfile).label)}</strong></p>
|
||||
<p class="muted">You can use tools-only mode immediately (live agent RPCs, no model download), or choose a local model if the SDK is available.</p>
|
||||
`
|
||||
}
|
||||
|
||||
function bindSystemCheckActions(card) {
|
||||
const actions = card.querySelector('.qvac-step-actions')
|
||||
if (!actions) return
|
||||
actions.innerHTML = ''
|
||||
actions.appendChild(
|
||||
btn('Back', false, () => {
|
||||
wizardStep = 0
|
||||
renderWizard()
|
||||
})
|
||||
)
|
||||
actions.appendChild(
|
||||
btn('Tools-only', false, () => {
|
||||
finishToolsOnly()
|
||||
})
|
||||
)
|
||||
actions.appendChild(
|
||||
btn('Choose model', true, () => {
|
||||
wizardStep = 2
|
||||
renderWizard()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function renderWizard() {
|
||||
const host = opts.els.setupSteps
|
||||
if (!host) return
|
||||
if (!host) {
|
||||
opts.log?.('QVAC setup steps container missing')
|
||||
return
|
||||
}
|
||||
host.innerHTML = ''
|
||||
|
||||
if (wizardStep === 0) {
|
||||
@@ -142,57 +212,67 @@ export function createQvacView(opts) {
|
||||
`<p>Local-first AI for PearData — models run on <strong>your desktop</strong>, not the agent and not a cloud API.</p>
|
||||
<p class="muted">The assistant uses live tools (<code>getHostSnapshot</code>, charts, processes) so answers stay grounded in agent data.</p>
|
||||
<p class="muted">Powered by <a href="https://github.com/tetherto/qvac" target="_blank" rel="noreferrer">QVAC</a> (Tether).</p>`,
|
||||
[{ label: 'Continue', primary: true, onClick: () => { wizardStep = 1; renderWizard() } }]
|
||||
[
|
||||
{
|
||||
label: 'Continue',
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
wizardStep = 1
|
||||
renderWizard()
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (wizardStep === 1) {
|
||||
const card = stepCard('System check', '<p class="muted">Checking environment…</p>', [])
|
||||
// Always paint a complete card with actions first — never wait on SDK import.
|
||||
const card = stepCard(
|
||||
'System check',
|
||||
'<p class="muted">Preparing environment check…</p>',
|
||||
[]
|
||||
)
|
||||
host.appendChild(card)
|
||||
engine.checkEnvironment().then((env) => {
|
||||
const body = card.querySelector('.qvac-step-body')
|
||||
if (!body) return
|
||||
const ramGb = env.totalRamBytes ? (env.totalRamBytes / 1e9).toFixed(1) : '?'
|
||||
selectedProfile = settings().qvacProfile || suggestProfile({ totalRamBytes: env.totalRamBytes })
|
||||
body.innerHTML = `
|
||||
<ul class="qvac-check-list">
|
||||
<li class="${env.sdkAvailable ? 'ok' : 'warn'}">QVAC SDK: ${env.sdkAvailable ? 'available' : 'not installed — tools-only mode'}</li>
|
||||
<li class="ok">Platform: ${escapeHtml(env.platform)} / ${escapeHtml(env.arch)}</li>
|
||||
<li class="ok">RAM (est.): ${ramGb} GB</li>
|
||||
${env.sdkError ? `<li class="warn">SDK note: ${escapeHtml(env.sdkError)}</li>` : ''}
|
||||
</ul>
|
||||
<p class="muted">Suggested profile: <strong>${escapeHtml(getProfile(selectedProfile).label)}</strong></p>
|
||||
${
|
||||
!env.sdkAvailable
|
||||
? `<p class="muted">Install with <code>npm i @qvac/sdk</code> in the PearData project for full Qwen chat. You can continue in tools-only mode now.</p>`
|
||||
: ''
|
||||
}`
|
||||
const actions = card.querySelector('.qvac-step-actions')
|
||||
if (actions) {
|
||||
actions.innerHTML = ''
|
||||
actions.appendChild(
|
||||
btn('Back', false, () => {
|
||||
wizardStep = 0
|
||||
renderWizard()
|
||||
})
|
||||
)
|
||||
actions.appendChild(
|
||||
btn('Choose model', true, () => {
|
||||
wizardStep = 2
|
||||
renderWizard()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
const quick = {
|
||||
checking: true,
|
||||
sdkAvailable: false,
|
||||
platform: typeof process !== 'undefined' ? process.platform : 'unknown',
|
||||
arch: typeof process !== 'undefined' ? process.arch : 'unknown',
|
||||
totalRamBytes: null,
|
||||
}
|
||||
paintSystemCheck(card, quick)
|
||||
bindSystemCheckActions(card)
|
||||
|
||||
engine
|
||||
.checkEnvironment({ probeSdk: true, timeoutMs: 2500 })
|
||||
.then((env) => {
|
||||
if (wizardStep !== 1) return
|
||||
if (!card.isConnected) return
|
||||
paintSystemCheck(card, { ...env, checking: false })
|
||||
bindSystemCheckActions(card)
|
||||
syncModelChip()
|
||||
})
|
||||
.catch((err) => {
|
||||
if (wizardStep !== 1 || !card.isConnected) return
|
||||
paintSystemCheck(card, {
|
||||
checking: false,
|
||||
sdkAvailable: false,
|
||||
sdkError: err?.message || String(err),
|
||||
platform: quick.platform,
|
||||
arch: quick.arch,
|
||||
totalRamBytes: null,
|
||||
})
|
||||
bindSystemCheckActions(card)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (wizardStep === 2) {
|
||||
const profilesHtml = PROFILE_LIST.map((p) => {
|
||||
const active = p.id === selectedProfile ? ' active' : ''
|
||||
return `<button type="button" class="qvac-profile-card${active}" data-profile="${p.id}">
|
||||
return `<button type="button" class="qvac-profile-card${active}" data-profile="${escapeHtml(p.id)}">
|
||||
<strong>${escapeHtml(p.label)}</strong>
|
||||
<span class="muted">${escapeHtml(p.description)}</span>
|
||||
<span class="qvac-profile-meta">${escapeHtml(p.chatModel)} · ~${p.approxDownloadGb} GB · tools ${p.tools ? 'on' : 'off'}</span>
|
||||
@@ -200,7 +280,8 @@ export function createQvacView(opts) {
|
||||
}).join('')
|
||||
const card = stepCard(
|
||||
'Model profile',
|
||||
`<div class="qvac-profile-grid">${profilesHtml}</div>`,
|
||||
`<div class="qvac-profile-grid">${profilesHtml}</div>
|
||||
<p class="muted" style="margin-top:12px">Skip model download and use grounded tools-only answers instead.</p>`,
|
||||
[
|
||||
{
|
||||
label: 'Back',
|
||||
@@ -209,6 +290,10 @@ export function createQvacView(opts) {
|
||||
renderWizard()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Tools-only',
|
||||
onClick: () => finishToolsOnly(),
|
||||
},
|
||||
{
|
||||
label: 'Download & load',
|
||||
primary: true,
|
||||
@@ -237,11 +322,43 @@ export function createQvacView(opts) {
|
||||
`<div class="qvac-progress-wrap">
|
||||
<div class="qvac-progress-bar"><i id="qvac-progress-fill" style="width:0%"></i></div>
|
||||
<p class="muted" id="qvac-progress-label">Starting…</p>
|
||||
</div>`,
|
||||
[]
|
||||
</div>
|
||||
<p class="muted">If this stalls, use tools-only mode below.</p>`,
|
||||
[
|
||||
{
|
||||
label: 'Skip to tools-only',
|
||||
onClick: () => {
|
||||
engine.unload().catch(() => {})
|
||||
finishToolsOnly('Skipped model load. Tools-only mode is ready.')
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Safety net — never leave the setup pane empty
|
||||
host.appendChild(
|
||||
stepCard(
|
||||
'Setup',
|
||||
'<p class="muted">Unexpected setup step. Restart onboarding.</p>',
|
||||
[
|
||||
{
|
||||
label: 'Restart',
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
wizardStep = 0
|
||||
renderWizard()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Tools-only',
|
||||
onClick: () => finishToolsOnly(),
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function startLoad() {
|
||||
@@ -249,36 +366,50 @@ export function createQvacView(opts) {
|
||||
const label = opts.els.root?.querySelector('#qvac-progress-label')
|
||||
persist({ qvacProfile: selectedProfile })
|
||||
setStatus('Loading model…', 'busy')
|
||||
const result = await engine.loadProfile(selectedProfile, {
|
||||
onProgress: (p) => {
|
||||
const pct = Math.min(100, Number(p?.percentage) || 0)
|
||||
if (fill) fill.style.width = `${pct}%`
|
||||
if (label) {
|
||||
const mb = (n) => ((Number(n) || 0) / 1e6).toFixed(1)
|
||||
label.textContent =
|
||||
pct >= 100
|
||||
? 'Loading into memory…'
|
||||
: `Downloading ${pct.toFixed(0)}% (${mb(p.downloaded)} / ${mb(p.total)} MB)`
|
||||
}
|
||||
syncModelChip()
|
||||
},
|
||||
})
|
||||
persist({
|
||||
qvacOnboarded: true,
|
||||
qvacProfile: selectedProfile,
|
||||
qvacMode: result.mode || 'fallback',
|
||||
})
|
||||
setStatus(result.ok ? 'Ready' : `Fallback: ${result.error || 'SDK unavailable'}`, result.ok ? 'ok' : 'warn')
|
||||
syncModelChip()
|
||||
showPane()
|
||||
renderSamples()
|
||||
if (!messages.length) {
|
||||
appendMsg(
|
||||
'assistant',
|
||||
result.mode === 'fallback'
|
||||
? 'Tools-only mode is ready. Ask about host health, charts, or anomalies — answers use live agent RPCs.\n\nInstall `@qvac/sdk` and re-run setup for full local Qwen chat.'
|
||||
: `Model **${getProfile(selectedProfile).chatModel}** is loaded. Ask about this agent’s health, metrics, or processes.`
|
||||
try {
|
||||
const result = await engine.loadProfile(selectedProfile, {
|
||||
onProgress: (p) => {
|
||||
const pct = Math.min(100, Number(p?.percentage) || 0)
|
||||
if (fill) fill.style.width = `${pct}%`
|
||||
if (label) {
|
||||
const mb = (n) => ((Number(n) || 0) / 1e6).toFixed(1)
|
||||
label.textContent =
|
||||
pct >= 100
|
||||
? 'Loading into memory…'
|
||||
: `Downloading ${pct.toFixed(0)}% (${mb(p.downloaded)} / ${mb(p.total)} MB)`
|
||||
}
|
||||
syncModelChip()
|
||||
},
|
||||
})
|
||||
persist({
|
||||
qvacOnboarded: true,
|
||||
qvacProfile: selectedProfile,
|
||||
qvacMode: result.mode || 'fallback',
|
||||
})
|
||||
setStatus(
|
||||
result.ok && result.mode === 'qvac'
|
||||
? 'Ready'
|
||||
: `Ready (tools-only${result.error ? `: ${result.error}` : ''})`,
|
||||
result.mode === 'qvac' ? 'ok' : 'warn'
|
||||
)
|
||||
syncModelChip()
|
||||
showPane()
|
||||
renderSamples()
|
||||
if (!messages.length) {
|
||||
appendMsg(
|
||||
'assistant',
|
||||
result.mode === 'fallback'
|
||||
? 'Tools-only mode is ready. Ask about host health, charts, or anomalies — answers use live agent RPCs.\n\nInstall `@qvac/sdk` and re-run setup for full local Qwen chat.'
|
||||
: `Model **${getProfile(selectedProfile).chatModel}** is loaded. Ask about this agent’s health, metrics, or processes.`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err?.message || String(err)
|
||||
if (label) label.textContent = `Load failed: ${msg}`
|
||||
setStatus(msg, 'error')
|
||||
opts.log?.(`QVAC load error: ${msg}`)
|
||||
// Don't leave user stuck on a blank progress step
|
||||
finishToolsOnly(`Model load failed (${msg}). Continuing in tools-only mode.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user