177 lines
6.3 KiB
JavaScript
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;
|
|
});
|
|
})();
|