Files
BridgeSwarm/examples/qvac-chat/app.js
T
snxraven 6cce90e0cf
CI / Build & Test (push) Canceled after 35m19s
QVAC Vison
2026-09-03 18:46:04 -04:00

177 lines
6.3 KiB
JavaScript

(function () {
'use strict';
var statusEl = document.getElementById('status');
var modelEl = document.getElementById('model');
var deviceEl = document.getElementById('device');
var hwEl = document.getElementById('hw');
var promptEl = document.getElementById('prompt');
var sendBtn = document.getElementById('send');
var stopBtn = document.getElementById('stop');
var loadBtn = document.getElementById('load');
var unloadBtn = document.getElementById('unload');
var transcript = BridgeSwarmExamples.createTranscript('thread', {
empty: 'Load a model, then ask something.',
});
var history = [];
var busy = false;
var modelLoaded = false;
function setStatus(text, kind) {
statusEl.className = 'bs-banner bs-banner--' + (kind || 'info');
statusEl.textContent = text;
}
function setBusy(on) {
busy = !!on;
sendBtn.disabled = busy || !modelLoaded;
loadBtn.disabled = busy;
unloadBtn.disabled = busy;
promptEl.disabled = busy;
}
function syncComposer() {
sendBtn.disabled = busy || !modelLoaded;
}
BridgeSwarmExamples.waitForBridgeSwarm().then(async function (BS) {
var has = await BS.qvac.status().catch(function (e) { return { error: e.message }; });
if (has && has.enabled === false) {
setStatus('QVAC is off. Enable it in BridgeSwarm Control Center → Settings.', 'warn');
} else if (!has || has.ok === false || (!has.available && has.error)) {
setStatus('QVAC unavailable: ' + (has.error || 'install @qvac/inference on the native host'), 'warn');
} else {
modelLoaded = !!(has.modelId || has.friendlyId);
var bits = [has.backend || has.device || 'cpu'];
if (has.deviceName) bits.push(has.deviceName);
bits.push(modelLoaded ? 'loaded ' + (has.friendlyId || has.modelId) : 'no model loaded');
setStatus('QVAC ready · ' + bits.join(' · '), 'ok');
if (modelLoaded) transcript.setEmpty('Ask something. Thinking streams above the answer.');
}
syncComposer();
var cat = await BS.qvac.catalog().catch(function () { return { models: [] }; });
(cat.models || []).forEach(function (m) {
var opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name + (m.tools ? ' (tools)' : '') + (m.vision ? ' · vision' : '');
modelEl.appendChild(opt);
});
if (has && has.friendlyId && modelEl.querySelector('option[value="' + has.friendlyId + '"]')) {
modelEl.value = has.friendlyId;
} else if (cat.suggest && cat.suggest.id) {
modelEl.value = cat.suggest.id;
}
if (has && has.enabled !== false) {
var hw = await BS.qvac.resources({ sample: false }).catch(function () { return null; });
if (hw) {
var ram = hw.totalRamBytes ? (hw.totalRamBytes / 1e9).toFixed(1) + ' GB RAM' : '';
hwEl.textContent = 'Hardware: ' + [hw.backend, hw.deviceName, ram].filter(Boolean).join(' · ') || 'probe unavailable';
}
}
loadBtn.onclick = async function () {
setStatus('Loading ' + modelEl.value + '…', 'info');
setBusy(true);
try {
var loaded = await BS.qvac.load(
{ model: modelEl.value, device: deviceEl.value, tools: true },
{
onProgress: function (p) {
if (p.percent != null) setStatus('Downloading ' + Math.round(p.percent) + '%', 'info');
},
timeoutMs: 0,
}
);
modelLoaded = true;
var label = (loaded && (loaded.friendlyId || loaded.modelId)) || modelEl.value;
var where = loaded && loaded.backend ? loaded.backend : deviceEl.value;
setStatus('Loaded ' + label + ' · ' + where, 'ok');
transcript.addSys('Loaded ' + label, 'ok');
transcript.setEmpty('Ask something. Thinking streams above the answer.');
} catch (e) {
setStatus(e.message, 'warn');
transcript.addSys(e.message, 'err');
} finally {
setBusy(false);
syncComposer();
}
};
unloadBtn.onclick = async function () {
await BS.qvac.unload().catch(function () {});
modelLoaded = false;
history = [];
setStatus('Unloaded', 'info');
transcript.clear();
syncComposer();
};
stopBtn.onclick = function () {
BS.qvac.cancel({}).catch(function () {});
};
sendBtn.onclick = send;
promptEl.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
});
var vision = BridgeSwarmExamples.bindVisionComposer({
thumbsEl: document.getElementById('thumbs'),
fileEl: document.getElementById('imgFile'),
attachBtn: document.getElementById('attach'),
pasteEl: promptEl,
});
async function send() {
if (busy) return;
var text = promptEl.value.trim();
var imgs = vision.getImages();
if (!text && !imgs.length) return;
if (!modelLoaded) {
setStatus('Load a model first.', 'warn');
return;
}
promptEl.value = '';
transcript.addUser(text || (imgs.length ? '[image]' : ''));
var userMsg = { role: 'user', content: text || '' };
if (imgs.length) userMsg.images = imgs.map(function (x) { return { dataUrl: x.dataUrl, mime: x.mime }; });
history.push(userMsg);
vision.clear();
var turn = transcript.beginTurn();
setBusy(true);
try {
var r = await BS.qvac.chat(
{ history: history },
{
timeoutMs: 0,
onChunk: function (p) {
if (p.kind === 'thinkingDelta' && p.delta) turn.appendThink(p.delta);
else if (p.kind === 'contentDelta' && p.delta) turn.appendText(p.delta);
else if (p.kind === 'toolCall' && p.call) turn.addTool(p.call);
},
}
);
var out = (r && r.text) || turn.getText();
turn.finish({
text: out,
thinking: (r && r.thinking) || turn.getThinking(),
stats: r && r.stats,
toolCalls: r && r.toolCalls,
});
history.push({ role: 'assistant', content: out });
} catch (e) {
turn.error(e.message || String(e));
} finally {
setBusy(false);
promptEl.focus();
}
}
}).catch(function (e) {
setStatus(e.message, 'warn');
sendBtn.disabled = true;
loadBtn.disabled = true;
});
})();