feat: add api key auth, devii agent, openai gateway, and admin service management

This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
This commit is contained in:
2026-06-08 15:38:33 +00:00
parent 921e382cbc
commit e0535bb7c5
270 changed files with 54408 additions and 541 deletions
+520
View File
@@ -0,0 +1,520 @@
// retoor <retoor@molodetz.nl>
import Queue from "./queue.js";
import Animator from "./animator.js";
import Balloon from "./balloon.js";
import { ensureStylesheet } from "./styles.js";
export default class Agent {
constructor(mapUrl, data, sounds) {
ensureStylesheet();
this._queue = new Queue(this._onQueueEmpty.bind(this));
this._el = document.createElement("div");
this._el.className = "md-clippy-agent";
document.body.appendChild(this._el);
this._animator = new Animator(this._el, mapUrl, data, sounds);
this._balloon = new Balloon(this._el);
this._tts = data.tts;
this._setupEvents();
}
gestureAt(x, y) {
const direction = this._getDirection(x, y);
const gestureAnim = "Gesture" + direction;
const lookAnim = "Look" + direction;
const animation = this.hasAnimation(gestureAnim) ? gestureAnim : lookAnim;
return this.play(animation);
}
hide(fast, callback) {
this._hidden = true;
const el = this._el;
this.stop();
if (fast) {
this._el.style.display = "none";
this.stop();
this.pause();
if (callback) callback();
return;
}
return this._playInternal("Hide", function () {
el.style.display = "none";
this.pause();
if (callback) callback();
});
}
moveTo(x, y, duration) {
const direction = this._getDirection(x, y);
const anim = "Move" + direction;
if (duration === undefined) duration = 1000;
this._addToQueue(function (complete) {
const clamped = this._clampXY(x, y);
const cx = clamped.x;
const cy = clamped.y;
if (duration === 0) {
this._el.style.top = cy + "px";
this._el.style.left = cx + "px";
this.reposition();
complete();
return;
}
if (!this.hasAnimation(anim)) {
this._animate(this._el, { top: cy, left: cx }, duration, complete);
return;
}
const callback = (name, state) => {
if (state === Animator.States.EXITED) {
complete();
}
if (state === Animator.States.WAITING) {
this._animate(this._el, { top: cy, left: cx }, duration, () => {
this._animator.exitAnimation();
});
}
};
this._playInternal(anim, callback);
}, this);
}
_animate(element, props, duration, callback) {
const start = performance.now();
const startProps = {};
for (const prop in props) {
const currentValue = parseFloat(getComputedStyle(element)[prop]) || 0;
startProps[prop] = currentValue;
}
const swing = (p) => 0.5 - Math.cos(p * Math.PI) / 2;
const animate = (currentTime) => {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
const eased = swing(progress);
for (const prop in props) {
const startValue = startProps[prop];
const endValue = props[prop];
const currentValue = startValue + (endValue - startValue) * eased;
element.style[prop] = currentValue + "px";
}
if (progress < 1) {
requestAnimationFrame(animate);
} else if (callback) {
callback();
}
};
requestAnimationFrame(animate);
}
_playInternal(animation, callback) {
if (this._isIdleAnimation() && this._idlePromise) {
this._idlePromise.then(() => {
this._playInternal(animation, callback);
});
return;
}
this._animator.showAnimation(animation, callback);
}
play(animation, timeout, cb) {
if (!this.hasAnimation(animation)) return false;
if (timeout === undefined) timeout = 5000;
this._addToQueue(function (complete) {
let completed = false;
const callback = function (name, state) {
if (state === Animator.States.EXITED) {
completed = true;
if (cb) cb();
complete();
}
};
if (timeout) {
window.setTimeout(() => {
if (completed) return;
this._animator.exitAnimation();
}, timeout);
}
this._playInternal(animation, callback);
}, this);
return true;
}
show(fast) {
this._hidden = false;
if (fast) {
this._el.style.display = "block";
this.resume();
this._onQueueEmpty();
return;
}
const style = getComputedStyle(this._el);
if (style.top === "auto" || style.left === "auto") {
const left = (window.innerWidth - this._el.offsetWidth) / 2;
const top = 40;
const clamped = this._clampXY(left, top);
this._el.style.top = clamped.y + "px";
this._el.style.left = clamped.x + "px";
}
this._el.style.display = "block";
this.resume();
return this.play("Show");
}
speak(text, options) {
this._addToQueue(function (complete) {
this._balloon.speak(complete, text, options && options.hold);
if (options && options.tts) this._speakTTS(text);
}, this);
}
async speakStream(source, options) {
this.stop();
let text = "";
const stream = this._balloon.speakStream(() => {
this._onQueueEmpty();
});
for await (const chunk of source) {
text += chunk;
stream.push(chunk);
}
if (options && options.tts && text) this._speakTTS(text);
stream.done();
}
closeBalloon() {
this._balloon.hide();
}
delay(time) {
time = time || 250;
this._addToQueue(function (complete) {
this._onQueueEmpty();
window.setTimeout(complete, time);
});
}
stopCurrent() {
this._animator.exitAnimation();
this._balloon.close();
}
stop() {
this._queue.clear();
this._animator.exitAnimation();
this._balloon.hide();
if (this._tts && "speechSynthesis" in window) {
speechSynthesis.cancel();
}
}
hasAnimation(name) {
return this._animator.hasAnimation(name);
}
animations() {
return this._animator.animations();
}
animate() {
const animations = this.animations();
const anim = animations[Math.floor(Math.random() * animations.length)];
if (anim.indexOf("Idle") === 0) {
return this.animate();
}
return this.play(anim);
}
_getDirection(x, y) {
const rect = this._el.getBoundingClientRect();
const h = this._el.offsetHeight;
const w = this._el.offsetWidth;
const centerX = rect.left + w / 2;
const centerY = rect.top + h / 2;
const a = centerY - y;
const b = centerX - x;
const r = Math.round((180 * Math.atan2(a, b)) / Math.PI);
if (-45 <= r && r < 45) return "Right";
if (45 <= r && r < 135) return "Up";
if ((135 <= r && r <= 180) || (-180 <= r && r < -135)) return "Left";
if (-135 <= r && r < -45) return "Down";
return "Top";
}
_onQueueEmpty() {
if (this._hidden || this._isIdleAnimation()) return;
const idleAnim = this._getIdleAnimation();
this._idlePromise = new Promise((resolve) => {
this._idleResolve = resolve;
});
this._animator.showAnimation(idleAnim, this._onIdleComplete.bind(this));
}
_onIdleComplete(name, state) {
if (state === Animator.States.EXITED) {
if (this._idleResolve) {
this._idleResolve();
}
this._idlePromise = null;
this._idleResolve = null;
}
}
_isIdleAnimation() {
const c = this._animator.currentAnimationName;
return c && c.indexOf("Idle") === 0;
}
_getIdleAnimation() {
const animations = this.animations();
const idle = [];
for (let i = 0; i < animations.length; i++) {
const a = animations[i];
if (a.indexOf("Idle") === 0) {
idle.push(a);
}
}
const idx = Math.floor(Math.random() * idle.length);
return idle[idx];
}
_setupEvents() {
this._resizeHandle = this.reposition.bind(this);
this._pointerDownHandle = this._onPointerDown.bind(this);
this._dblClickHandle = this._onDoubleClick.bind(this);
window.addEventListener("resize", this._resizeHandle);
this._el.addEventListener("mousedown", this._pointerDownHandle);
this._el.addEventListener("touchstart", this._pointerDownHandle, { passive: false });
this._el.addEventListener("dblclick", this._dblClickHandle);
}
_onDoubleClick() {
if (!this.play("ClickedOn")) {
this.animate();
}
}
reposition() {
const style = getComputedStyle(this._el);
if (style.display === "none") return;
if (style.visibility === "hidden") return;
if (style.width === "0" || style.height === "0") return;
const o = this._el.getBoundingClientRect();
const bH = this._el.offsetHeight;
const bW = this._el.offsetWidth;
const wW = window.innerWidth;
const wH = window.innerHeight;
let top = o.top;
let left = o.left;
const m = 5;
if (top - m < 0) {
top = m;
} else if (top + bH + m > wH) {
top = wH - bH - m;
}
if (left - m < 0) {
left = m;
} else if (left + bW + m > wW) {
left = wW - bW - m;
}
this._el.style.left = left + "px";
this._el.style.top = top + "px";
this._balloon.reposition();
}
_onPointerDown(e) {
e.preventDefault();
this._startDrag(e);
}
_startDrag(e) {
this.pause();
this._balloon.hide(true);
this._offset = this._calculateClickOffset(e);
this._moveHandle = this._dragMove.bind(this);
this._upHandle = this._finishDrag.bind(this);
window.addEventListener("mousemove", this._moveHandle);
window.addEventListener("mouseup", this._upHandle);
window.addEventListener("touchmove", this._moveHandle, { passive: false });
window.addEventListener("touchend", this._upHandle);
this._dragUpdateLoop = window.setTimeout(this._updateLocation.bind(this), 10);
}
_calculateClickOffset(e) {
const point = "touches" in e ? e.touches[0] : e;
const o = this._el.getBoundingClientRect();
return {
top: point.pageY - (o.top + window.pageYOffset),
left: point.pageX - (o.left + window.pageXOffset),
};
}
_updateLocation() {
this._el.style.top = this._targetY + "px";
this._el.style.left = this._targetX + "px";
this._dragUpdateLoop = window.setTimeout(this._updateLocation.bind(this), 10);
}
_dragMove(e) {
e.preventDefault();
const point = "touches" in e ? e.touches[0] : e;
const x = point.clientX - this._offset.left;
const y = point.clientY - this._offset.top;
this._targetX = x;
this._targetY = y;
this._clampTarget();
}
_finishDrag() {
window.clearTimeout(this._dragUpdateLoop);
window.removeEventListener("mousemove", this._moveHandle);
window.removeEventListener("mouseup", this._upHandle);
window.removeEventListener("touchmove", this._moveHandle);
window.removeEventListener("touchend", this._upHandle);
this._balloon.show();
this.reposition();
this.resume();
}
_clampTarget() {
const m = 5;
const bW = this._el.offsetWidth;
const bH = this._el.offsetHeight;
const wW = window.innerWidth;
const wH = window.innerHeight;
this._targetX = Math.max(m, Math.min(this._targetX, wW - bW - m));
this._targetY = Math.max(m, Math.min(this._targetY, wH - bH - m));
}
_clampXY(x, y) {
const m = 5;
const bW = this._el.offsetWidth;
const bH = this._el.offsetHeight;
const wW = window.innerWidth;
const wH = window.innerHeight;
return {
x: Math.max(m, Math.min(x, wW - bW - m)),
y: Math.max(m, Math.min(y, wH - bH - m)),
};
}
_addToQueue(func, scope) {
if (scope) func = func.bind(scope);
this._queue.queue(func);
}
_speakTTS(text) {
if (!this._tts || !("speechSynthesis" in window)) return;
speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text.replaceAll("\n", " "));
utterance.rate = this._tts.rate;
utterance.pitch = this._tts.pitch;
const voices = speechSynthesis.getVoices();
if (voices.length > 0) {
const match = voices.find((v) => v.name.includes(this._tts.voice));
if (match) utterance.voice = match;
speechSynthesis.speak(utterance);
} else {
speechSynthesis.addEventListener(
"voiceschanged",
() => {
const v = speechSynthesis.getVoices();
const match = v.find((voice) => voice.name.includes(this._tts.voice));
if (match) utterance.voice = match;
speechSynthesis.speak(utterance);
},
{ once: true },
);
}
}
dispose() {
this.stop();
window.removeEventListener("resize", this._resizeHandle);
window.clearTimeout(this._dragUpdateLoop);
window.removeEventListener("mousemove", this._moveHandle);
window.removeEventListener("mouseup", this._upHandle);
window.removeEventListener("touchmove", this._moveHandle);
window.removeEventListener("touchend", this._upHandle);
this._animator.dispose();
this._balloon.dispose();
this._queue.dispose();
this._el.remove();
}
pause() {
this._animator.pause();
this._balloon.pause();
}
resume() {
this._animator.resume();
this._balloon.resume();
}
}
export async function initAgent(loaders) {
const [{ default: data }, { default: map }, sounds] = await Promise.all([
loaders.agent(),
loaders.map(),
_loadSounds(loaders),
]);
return new Agent(map, data, sounds);
}
async function _loadSounds(loaders) {
const audio = document.createElement("audio");
const canPlayMp3 = !!audio.canPlayType && audio.canPlayType("audio/mp3") !== "";
if (!canPlayMp3) return {};
const m = await loaders.sound();
return m.default;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -0,0 +1,18 @@
// retoor <retoor@molodetz.nl>
export default {
1: "data:audio/mpeg;base64,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",
2: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAsAAAUZADMzMzMzMzMzM0dHR0dHR0dHR1xcXFxcXFxcXHBwcHBwcHBwcIWFhYWFhYWFhZmZmZmZmZmZma6urq6urq6ursLCwsLCwsLCwtfX19fX19fX1+vr6+vr6+vr6////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAERgCxlVBGABpeOEAAQEB+CByCETn3ZQEwfB94IA+D4Ph8Rg/BAMeDgIHOoMet/+D4IHPLg/B8H/E4P5cHwfBwEAQBDzn3fynl3//ggrfoNfAMmppHbcjFOkREnlKl3EWUWcP/+MixBYXygrsAYMoADJOOPZBEMEGynGmcxHQSdnQz0LSJndrpxAwldtSLftPIssh2noyPZBHNi7wneDYIgFDREVPO8gaGPzr5SAYuwUHf1f/hYc8KvXVqqmazxvyD9Vh5qwPHcpUUKRM/+MgxBIWmVbZi8MYANAIHsbKDBp0kigU/CrxyjU4wMur/5mO1VcrhQ0WSCo8sVO5FT4KuWE8OFgaPFeDQMnTtzXckE3ezhoaeCgURjRqx0qGv/d/uUrRHxbLSRUikq9WmJNhVUxjqjP/4yLEEhhh/rDCSMaYSq5MKUTkEUBEt4VFEsRdKhhRsBAWFUyZDvlqFAc+W5QCPyz857ExlIzQvOMZcqleUBNK0iIBNBqJopLXuc+vRlk0aEWI72sb2Wt/c4n9gEotfYCYcYVVoZSGAg7/4yDEDBawsqDKGMZMDUMUagJWplWxlBCuQTsKMGzBPWJwZERNaBYubHB4KHWzqa1xAIHiEOgZo0JEQ8wXAyFNehy0H2Lok+gpQ526uPrRu1uFvS4XculbXf0srh//wJ5hbCsZUEhRqP/jIsQME9AuoMoQRAARmnkToWeDFqaT8WeA1lC0UeZi9k+KiECKKo3UqFN0kapQkJpXEM7ZPtHdiE77LGNfTUqPsWBEajbnrNZBNjVy9xRqakv///9lZZ0z2BTeWaEowJg9UBmhA84+Hv/jIMQYFGheqZQIxiigaWHKCLybxzRUMPTeah5tAwHjLgyRiYIIa+xSVqKp8xFAOlSqdrPftUldC7ZmRRMpU+99yDSf/Y9dBoqqqfIdKiNIO0Lg2BBUCC4DEoGQpTSgYFz0wBDYIvYg/+MixCEUqAahkgBEAKnwoJhPIlUHzh9WPeGHrPUFXvt1HETKDTDdMTbmXTECDUJlUzyid/Why419qUKSk19KB3//8GST5FOcW4RCyd0kJxA57iaG3XMjgxKCp8BtWfEFCb19Vx/eyHyL/+MgxCoUcJ6kAUEQADuIHwiFwIJ2H5RY51z3ROQCwAAZRlsPOWWBAToUTKCeGFr+gPkBAJ4CIYYIBgQCyBCb1XyhPMJCb8HscB7/jw8OiKFP/NOKnkV//c0bDYqKRt//joiuAKDwVA//4yLEMxijbpj9gjgBhkB3//8fOMc0IQlZRQItfp//7IPD5Sw2KCUxopLJ////+OkUKmTXKjhVUGzoJ3AqTEFNRTMuOTkuNaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqr/4yDELAAAA0gBwAAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg==",
3: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAsAAAUZADMzMzMzMzMzM0dHR0dHR0dHR1xcXFxcXFxcXHBwcHBwcHBwcIWFhYWFhYWFhZmZmZmZmZmZma6urq6urq6ursLCwsLCwsLCwtfX19fX19fX1+vr6+vr6+vr6////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAEkB+eAlGSAIn//aP4jO93bXbEAAAAAgeCAYJGHfLggCAIAgCYPn8oCAYQX+c6wfwQd///E4EBAEHcoCAJg+D4Pn//gg4oCH/4P8oCDsMeJwfQ89XeJbWwxIOXMB8lFxKUAIC/+MixBIU4JasAYZIAMszld2+yeo2QEYqJab3FRVmRR45aj41iHXpcezzNNFEVeZLMTHnmTNS0Z3CBsYhlnMmmlFuhzvYRvF21r87JyKplakAKqr7j9bzuHJEkb30znIvIxgcXJUF3YAq/+MgxBoZiP6pk89AAM5oysIDRQUIHB0FXPhYieKdd7qbtu0XciXoqW95pyErsOPYVehpsHTwCGmxFFK0OJHHvXJs3re9rK5XajliGN3bOsi77967FtSLMfQq4D/41inbGjkCtnkMZXX/4yLEDhfYpqzKHh5M5qXLARxpIjapmWpOm0lA8B5Hg2u6q9yc9z41SYwYA8fFBV5xziZ8o5LE1urymoHGOsJVNTaj7cwbRa9VLdleK1q0JPSK7X2dNiscrWmb7EC9VZAACqqqnJzkMt7/4yDEChbw7rmUZgxwpIFYJ0nF8AZAYsChNfQ0BaNX60EcYvHotT1r7sZVr6AqrJtxusme8F3rP+Z1FaKhpj0Xm1T6xrFr0Vq3bS2iTOeR1fu269/3/Z+v9VNcl33opbEAATLdtttqrv/jIsQJFYia1l4zzEzYEoUsS8Vj5mLELOKSiXCEzRXEdvpnma7LLxiR4GwTCaQCeYHSI5ryqAUIjluQy/4oh91NNHRsJDbu39V0dU5aEspOsr9NdCquPZ9X9aqNAFS222222qVCNkVSB//jIMQOFHBe8l4IxiqGoZiXwJAmEJcRLAYQPC+5pRT09SHYbKRZE4lyHMMqHbDBEsTchU0hiHVTStuw/t0Of0CJC219FOd67e0UdUeSiUueSgX/eV9lIOKQsyUyACiFtDmxvQxjjVc4/+MixBcUiHKoyhvGSCQUjUAKZQmWvQkWfZO5JpxOxJpa0UBZIfaetb1elgx7Hp6ERZuXfcZQEnWvOWLaqvpZal9VWukffHu0VaqqqRGP//5FCIiB0Mno4YzELOHhCu5Qo9zlvXURtCCG/+MgxCATkFqoVUkQAIXSzQdYdvkw7quFkUKZFiUbIk1PM0OFHLc6srWpp2mR93oRnmVOYadsXkaN/ZpVAAggQAEScnCUPETCmBZ9B8v8487/NSrf+QmExzr/+QlDyEtN//wlCcIIbjj/4yLELBczdogVilAASkxqf//qLmIR6RKzCsI5F///+4lFgbiqlCcViYmFYjZP////yIvOIdDTTEPMVpEPnUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/4yDEKwAAA0gBwAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==",
4: "data:audio/mpeg;base64,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",
5: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAkAAARIADw8PDw8PDw8PDw8VVVVVVVVVVVVVVVtbW1tbW1tbW1tbYaGhoaGhoaGhoaGnp6enp6enp6enp62tra2tra2tra2ts/Pz8/Pz8/Pz8/P5+fn5+fn5+fn5+f//////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAE7nidAFBEAD/gAv/jG/1PnOc/5znOc53b/ITOc53/zgAAII055z/+QhCEIc5zvoQDFuIAQBA/BB2D79QIAgCYPn/xIcxAD4Pn/4gBBwOAgc/qBAMQxggVV/Pc++tvRmalfpM/+MixAwUMhq4AYFQANFSzKe6k7GmLCQSSqERG1RYLI46h4s5osGDgiiUqQ2M9Dj1OMZVc8fqcTmuYTohImqegtqTNCwu4TChX/zjP1OZ/6vvpnLbbbboqkjZtTsrV1NKyUHAV+S0JCRN/+MgxBcaAg6+V8JAAMwa0ptbVUPD3N99/8XdfRIuFZ6xIJSYb+u55lY4Xhvqf2uZNq4nYf3dxf7GnmNsULVDlCMilwXU4q4UmEZokSS2lAqtNDCTFR6337V2jWL3m7//5A7miSjMpZL/4yLEChXRarGUCMaUAihSZnsZ0yQF+TmiddxYgAEIiIiO/E+aIhFyQHTtUtRTGBgwpCCEyR4KAIDPQiL8XADBUTtZUK6iTOxN7HPtplyFf7Siv0bEXf27GLpqcul2kAac7pfhowoUMRT/4yDEDheBErZcGYxwZOOaSZLvKLRp1SRCc+HBSW55ma3tXeZqqpJ581kmyx0isFTtqBKDTxQUzrgqRkKWUsA8JW1C+bbfRt1RoCetSbGOQJXyKfYe+rQt0XchyOnDf//fb7a2SgD7kP/jIsQLFHE2+l4IxnLGWYiXd7OX0RNNAjmEimZ17CqbHH9DEG9dKT1ziuaCATHyDir4rIDZIYNcKIWEVOStSe+XA6LUME7Sa59t6tP/9//9e9/QYv///61T9E/1dKyv99SEVz1Svk/kV//jIMQVFWOuwAFAKADrQQIvot1Ogo6znOel5DmazsdxNGZZiiCs90Qgwp0KY18iDahAVMSfYxEVBAokJDXVF0eSX1S1WoPKIqIBYYh+qjr////f//X/0pb//Jmq11T/ZtqEZWVKOcFm/+MixBoYa6bIAYEoAPVCoPSOUB1fdg44CjgiZnW9dIiK4jC2ZRcjiYbOooKuBgmBwI9xrq6h8RNlKXsYRMLZSoVCisfGGGoe+5LDxzKrsLSdLa6stUBACAhBBJIIEBfL8/Wv6sd//Hf//+MgxBQRSp56LYE4AFr//07jq//845zZoqHv//y41HTDzlHkOFI7///+YecaKhsoRCouwaZ//wkwUUFWWMUqTEFNRTMuOTkuNaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=",
6: "data:audio/mpeg;base64,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",
7: "data:audio/mpeg;base64,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",
8: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAUAAAKmAGJiYmJiYmJiYmJiYmJiYmJiYmKJiYmJiYmJiYmJiYmJiYmJiYmJibGxsbGxsbGxsbGxsbGxsbGxsbGx2NjY2NjY2NjY2NjY2NjY2NjY2Nj//////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAFGGKoidBGACMATACpHGMf/8YAL4Hd3d3f/QvroiIXogGBgYGLf0d3OIAJQiFoEAAAQLd3d3c+uHAwMDcHAQBD+Xg+HwQOf3f4PviB0HwffUCDv/Lg///+nkFVf//JtszOzTM/+MixAkUIX68AYFgAJmZpkztWcq1+S9FnzNYkItj5AE6hWZOEGiHEAwv0K4xLQloIhmRJsRz1uTKxWE5WbDrArisGgWBpfhn/d+/9TP/0f///9/+lUSr3xetKtl6lLso21J+plLU1rvr/+MgxBQWwX6sAYFoAC4J2SxgXaxdNDJBpYUV3SHwKVSiGiiZmKlGzJkUh0zc1nD9Uetda0rKWm3jX8ilYsh9A5exeX891pqtIpdqSxe3FT1uL2XpBQ+nBRHxQW8xo/pKSR/zJMnFpR//4yLEFBezboQBhWgAV81LoJ8kYrV/8zKJFE+LRdEy16K/5RLyLnjU3PK9X/8xpzVTrSMWO////oHVs6aBskkZHD6zb////8wLyJiYGR00QTrMls5go3+pTEFNRTMuOTkuNVVVVVVVVVX/4yDEEQAAA0gBwAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==",
9: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAwAAAWCAC8vLy8vLy8vQkJCQkJCQkJVVVVVVVVVVWhoaGhoaGhoaHt7e3t7e3t7jo6Ojo6Ojo6hoaGhoaGhoaG0tLS0tLS0tMbGxsbGxsbG2tra2tra2tra7Ozs7Ozs7Oz//////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAD1AGfbFBEACo6gABAAEAQBA5qIZyXBB2CYf+IAQU4uH/TqBB3+sEHZcHz/5cH+GPiAEDniN5T8Ez//+oEPygIHPhj5cH1WRrW0okEn7XPctERlZmO/UWVqiIY1J7Pozs6Wur/+MixB0bVAKsAYUoAKOjEvTsplQOVTZjpTVEFkMRBasYR3cdQTF6szudDyPOo4UozlhE7sgYncedkS9vqQRSyoVqoRCtFx8qqyPKm9nPPbXWjvPerTadlrZ++e7D1aKpqqkstq85UYOn/+MgxAsWChKtkco4AM5Wo+qkE/+lXZ6zUflRwylJx9FUwXvzKUeNgXljmeqUfOVaujVShw618748cx/fzSofdXScb1CgPhvwAHGDn7vpG6dAmb8Z4upCtr7kqqqqqqoo57elYiAjaUf/4yLEDRbLXq2SKcS8+56bO7TVMtOXX1MPmXuiURQeiW2/OdB0Ktba9lRJn+ybkFsnR32Urv177X36pdQRL6N0UI7f3ZDtdK60cknv+z/+6Dgm/zRzL5wzeemkVc/////oiyt6X0CeTs3/4yDEDReCxrWUUcq4s9upe9t1ZLi/zrLS5hN3Od9dBMAUQ06XqwDnlv94kuyWXoNF91ZkmzGdmeyOqNUXvuf7ORqbfvP2t0DysY4dJiAa0RtgH+j55zS2oYTcK6Fpb0/gsf29Ktp61P/jIsQKE/OqpAA4jtzwttl+qG9nvzAy+3usGb3/YzvrmeS7N1rspy03z9ntb6Majd+/ktGf1uezGP5ymDoyGF06ohpjv+v7aMnVHOJGIpoLvY6h/////+LHyrZcNq47OgNHXfxNxXc83f/jIMQWF+uqtZRaBNx9daXbenzF8JxXU3HE8UdUTDRT7LC0Ds3KYya5imGulQtDznTRd/R7H2+ZqGlat/1Tf/Usv///qFG/utq+tvy9/9RJDSD0tPa4qgYmvdzIA0chrULE5+BiqhYS/+MixBESEDqgADhUBBlBkAi0yLbyIzw2utli5ZfP2LdYKY66hG0rocIV01oRdYcoINbWoUQPfaxqVqwJc4m6LWP26Vpqv/+Aa2kvWzrazBgttZkJZemU4aeWp9vYeTPQxNz3RqVdPdqP/+MgxCQUacqgykDElHq2anQ7zKdL9qPukLUdSg2cNmSwvE7W7HbCXvbZWctRW7jgrbe9+jmKH8UyD03//0CM74xZg6hgBFZpAEMi54BFw6bFhrHSMjebNQ4Bng+T4DUOWsoo1CblpOH/4yLELROoPqDLQhAAFRpg5oshwzodY9q0r2o3rpYepG5BXzH05pT4unsertKPSgZWSNvHktNvxKbpHvGglDmdzweg5GysvwLD8bg6NHUc35UgRGgGiTQeQ7+OCKFRqOliKHdP88LIPFz/4yDEOiBjeoABizgBkOEBV9WO/8mLTh0gWcUk0OIedm9P/yQ2FAsPQaCQPDRhsRUkcdHhsrDyzlr//4pCg4SB8OOIsmWHhqD4SBlh0sWXh2/vCKFKTEFNRTMuOTkuNaqqqqqqqqqqqv/jIsQTAAADSAHAAACqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg==",
10: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAQAAAI+AHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc6KioqKioqKioqKioqKioqKioqKioqKioqLR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR/////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAETJuillBEAAFGIkCRonmNuQx3KQBjGMbxkIT/9T/znORv/59TnP8jc7/////rO///+hDnOd+cAIcB8H8EAQcGC4PvEYPv/L////+oEEVgkMabVbm66Uvr2vtV3butvmR4WOc/+MixBYZe96sAYU4AMOlOiPVtVez3dGRUsjta1UU2c/+y37uZR0ckp9q77R/7U46BR0HhYaYM6MyIDDpQnKC41rTItZpyFBtn512pfmN26Guqa9+y+bdFcq4U6w6ggLSHguHfQ/x5/5p/+MgxAwWu3Z8CYc4AKyf5ynKb/46YSHgfE//x4lnHf/9Dh1kNNNHv//xsIw6RFRpQbR0ahD///+ASNAhByWYIhKHiQpHiR3////9JxJR0iaPDYqPHkR0wRhqUG1MQU1FMy45OS41VVX/4yLEDAAAA0gBwAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=",
11: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAwAAAWCAC8vLy8vLy8vQkJCQkJCQkJVVVVVVVVVVWhoaGhoaGhoaHt7e3t7e3t7jo6Ojo6Ojo6hoaGhoaGhoaG0tLS0tLS0tMbGxsbGxsbG2tra2tra2tra7Ozs7Ozs7Oz//////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAEegCcjFBEAARCpTGYDETw9dUD8Tn/1ZQEDn+oEEB/8oc/8Tg+f4nP+CYPg+H4Pn/lAQB8HwfygIOg+D4f/DCwfB/8Tg+H/hiXB9+CEhB8Poa0b1r//Vvp7rWntsT3f+nKnq+/+MixBMX8+akAYIQAcm/si9n2vXdqyao1zFo+1Ps57Uoys87FYhnRTM2ZmbUiqdWNncO7IRbQYNBIJnGRGd5EbIjMRVIrsyoYjOrYhCSQbJYxitMZwazfS5KalltttsikCl6olp6plYu/+MgxA8X6ZquW8UwAK7ilz29S7ZkJmF+D9WpoQc6h9kkC4lMrULM+xvUyL+Np3vo6fqNx8dcJ1lm0YJhouaFxgP4SW9SSy4Rv74srUcU2rUnjHqywstCuU7+v25XRXBZttbG0pAnt7L/4yLEChYg8q5aKMZww+7OgekzqBL+WsJZWZSgRXX8yYILlEEXaJGBgakCtGFg89IOmmQDclBstcQj4x8k9SRVwp70wwwlA3S1iFFX4etp03jXS97FPEDZP0UetZsP4hobS2RJMbvr8LD/4yDEDRaJNrp+EMZwRYcnKIGBfTNyBsosFXd+/DycVIDLe9tbRzRcqeYYVaHyQbHZxQIC7x8ZAznMe8OLaXLFyAxBZ/ZyS0W0EmuUgsReqj2+xvF0J1f1uxKqg//76vBn/eRiTynhDP/jIsQNFnGepZIozJTOMlc45o6L9P3Di7lmiC9wMckgZytlBmRaq3Kh1XENV53qnzNftjtvKfE4lSJSp1qZ7lWFHLDa3PGilg5GtotI2Or9xBd/qdO6UVf/or/fgGfZUVBITOYReHnuXv/jIMQPFEj+mMooRnC+c7GYpAzCj1Zo0qic3PLawedFyIu4MIWrhSixJtTRR+deSbtQWJLap1GKIUWGxyla1sJXs1X5VVqEY8d4dm16GsTVg/+gb/W4XBdVgCaYwyC40IsZiFh5oOmn/+MixBgTsAaYyjBGAJAJXtFiATKkazSiYUnsk4Gyui6OFAJU4A7LPKyYfSSPlmLJBNiy9Lj525S7+/rC4ykXurWlFimb/8Bn+py96OtmHlXnBr0BkUeTgIUDFITF40IUrewBBCLIURUP/+MgxCUT6G6YyihEKK1pTtpExmLrfQAkhoqgnaOZYXGpsSZdapx2SXnlOm7kMQs4/IWUe6OqXLqP/4Bn/9H15I4CCyBUTDm1kxVLTZcmDopEo4ChcSHWz5lbCLMe15YXJObUJGrU5a7/4yLEMBPoZpjLRRgASaxxFFPIvNTKqLyz0pES4K2e4AJbL6LbPAGhqK6HNMUNWGoWoabjeq9lCgtTWMBYIdC3+LDgCCIoKIg34oEDYBa6uJ+HPFHQZi/ILapfw/gcFsMFLtfMIZM1Y1b/4yDEPCBTtogBiBgBPxBBsgxG44lgQqsDkgKUtfrCj9W0dHIuBXxSvUqmRg3kMGSGv+a/6rgyIwRsNHqo18OLodHDDxzsHzHIDoi8qf2F4rL8lQSCRgYCY9f/1KMxRj6v+WhjMhn6Gv/jIsQVEXGOHYHDEACG+b9Slepf/1L//M6lAR4alToKuiX7VPBV+VOiXEv/h2JTold/ywNKDv/+oGhgNfh1TEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==",
12: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAUAAAKmAGJiYmJiYmJiYmJiYmJiYmJiYmKJiYmJiYmJiYmJiYmJiYmJiYmJibGxsbGxsbGxsbGxsbGxsbGxsbGx2NjY2NjY2NjY2NjY2NjY2NjY2Nj//////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAEXgGnlFBEAAtqhC1kICsHwfDCMu/ggCByUOQfeD/BMPxA7sLh//KHP9QIYgDEoc///B8Hz+JwfNS4f9QJh8QAgCAPg/iMP/DH/gQEDkmGYaNvfIRBcW/2+Z/cdczizyZmhyo/+MixBUXwyq4AYI4ADhB3xsQ8go4OXOWJiveUGj+q5typyv//8sJAsVkPOIotxuWlkp6hGbbv0U05TUUsLzEir/NiYvv9K4Vbl3B0GgeWx7QCd9an7PqDIZDKvTGoKCVFy9VSlE6/Eg3/+MgxBIWGxKsAYU4AChHjrfWEQb6lAbzYVPEm+4hIv0enp/Q/+76/jhEuJ/fjwOQcETdeaPGnf+3MKlyoAxyffx0F4ZX9e4+IoeV+k798VAvt8RVFZBRtstLpW5OW36LomGkrxMaKnP/4yLEFBgLXowBjWgBJKtH6bGBJF8NPrJH8huamrMd0v/sYmFBaVKl+v/oDJHqICYH1m5izWMlLLqkv//rrMkFIIkmF4JbW36kjFkv//6DVKUtdFRqXsriuSpMQU1FMy45OS41qqqqqqr/4yDEDwAAA0gBwAAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg==",
13: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAUAAAKmAGJiYmJiYmJiYmJiYmJiYmJiYmKJiYmJiYmJiYmJiYmJiYmJiYmJibGxsbGxsbGxsbGxsbGxsbGxsbGx2NjY2NjY2NjY2NjY2NjY2NjY2Nj//////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAExEijgNBKACywOBgZjGN8HyGP8BznfyHfIQhP6EIcPgOAYBh8+8hGU4ucEDggDHiAHwfB8+XB9YPvD5QEHAmD5+c//8uH3f4fggc/5QMfUCH/BMH1fXXzP/xrEO/xbH+vvev/+MixA4WedbEAYF4APG//v08G2twaXlNxkyn6UwjYIp7uZfe3i01TWr7v0g6233rZ7nEaNbFL3w2RP5YUGS9P6zXrNeAKhR92ETD20ta/o6v9n//T6/q/yLmv8je5v1rOZMzPbNL17fn/+MgxBAWYdq8AYFgAGZvt8lzuvq9fCXC+BW1nh5IILR8tKXC13oIIWcZ1qDFHq3lfWXPrq0ukdmm9fXT69Nyi2i3qVhJCWvj0v616NX2muxf/30LV/lqEEkD8y819Re/MhzieMf8ulj/4yLEERjbXowjgYgAFmigRCv/jHD0BAgJgH4DY9rX/lMMuhllFEhpDv/8QsQo5REklLI4mf//zQ2Mi85ianETI2Jr///82MUkq0WRRUiXS6l/////R0UnMUkkpkXpUZVMQU1FMy45OS7/4yDECQAAA0gBwAAANVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==",
14: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAkAAARIADw8PDw8PDw8PDw8VVVVVVVVVVVVVVVtbW1tbW1tbW1tbYaGhoaGhoaGhoaGnp6enp6enp6enp62tra2tra2tra2ts/Pz8/Pz8/Pz8/P5+fn5+fn5+fn5+f//////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAEAiuhNVBMAAGGGBjxjGMYwAAAAAJDGMYxjf97PJk02JpmAAAAAgCB/lz/B9+XP/4gDHrf/yjuD4PvlP/8Tn///z+UDH8oCH0Khom3fq1J9sbuCyKaCkq1ZGAwlc8LmTZGPS2/+MixBoXwNasAYJ4AJeBznGuGSOS8v7+23NXIRTmnmShgYqtKzg1jWpuCLr3wslkWC5IsLcbUEjpg+Yr8okv35exNHCZKPehPhLu121rTc1tKsD6A///xv/17WMIBngB8PK1V8lVxcsC/+MgxBcaEV7FlcJ4AB5oMpO15kJgJgbi7ThuCYK1uZHBWOoEffr/rGqazTecvI7x/CifPrH+JQKDr7BY0LATCzTgEYaUGxjCgbtXTW/e1dP51LKFa1143K+3V+uteupMyA//8v5/7/X/4yLECRbY9sV0Chhw8UISAfD8PA/BuBhDuByTjNwkCeVwoJYjKaB8cFd19t9dSk5HZ4spVxiGQmcFCJaJAOLjFKNoothp+D4y1NvcU1MPtT2IWyxb1ze39mtyK9X/fTVgQSEAAySSSKX/4yDECRa5qspeCkqU+v196qGNIiZbWn2wpNtEYbdJZygmLGz+BQJiBQiKOGsiCZv/an7IZWR1EcrFZzaIcUHkHlCosWAe8jd3pGO3lq0p3dfsb0P7O4t7ko1+26ua1oNtsyQ5ttraN//jIsQJFgjy0l4RknD2v+bux9n+cKB3Rg9Tqt7QjYUQEhRdvNbbQdVlQzmtFWWEj52HlJSPIoW2WE6BDDp5SkiUXGPSx0cQqGgO1DU77zEW29XfMo4zv8tt/v8T1Rv/0rftWGfNILhlsf/jIMQMFHjqpAFCeABGdKPLcWM6W8YAAuDZOkqHieS6tXL2FS2H0eo4KKgYs9GXPBkqPULiQGjyUKnpvNLWtbK7L2Ja6y5ScYhXf+3vrjdn19fWDFvnbzopcqYmdzRWBuJengVQBQNQ/+MixBUYU3aUAYVQAF8SkxCpqfInNJkHuhppv7uRGljE9Dv89zZx55h3rX/5Kaeikh7Exy/p//+hYoiHmnqYS0Jc75v0//+VRzjR4YSnHHE9jZpKSrLHi2SqTEFNRTMuOTkuNaqqqqqq/+MgxA8AAANIAcAAAKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=",
15: "data:audio/mpeg;base64,SUQzBAAAAAAAGFRTU0UAAAAOAAADTGF2ZjU0LjUuMTAwAP/jMMAAAAAAAAAAAABJbmZvAAAABwAAAAMAAAHVAI2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjcfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx////////////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jIMQAAAADSAAAAABMQU1FMy45OS41VVVVVVVVVVVVTEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MixFsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MgxKQAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=",
};
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
// retoor <retoor@molodetz.nl>
export { default as Bonzi } from "./bonzi/index.js";
export { default as Clippy } from "./clippy/index.js";
export { default as F1 } from "./f1/index.js";
export { default as Genie } from "./genie/index.js";
export { default as Genius } from "./genius/index.js";
export { default as Links } from "./links/index.js";
export { default as Merlin } from "./merlin/index.js";
export { default as Peedy } from "./peedy/index.js";
export { default as Rocky } from "./rocky/index.js";
export { default as Rover } from "./rover/index.js";
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
const loader = {
agent: () => import("./agent.js"),
sound: () => import("./sounds.js"),
map: () => Promise.resolve({ default: new URL("./map.png", import.meta.url).href }),
};
export default loader;
Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

File diff suppressed because one or more lines are too long
+115
View File
@@ -0,0 +1,115 @@
// retoor <retoor@molodetz.nl>
import AiClient from "./client.js";
import ToolRegistry from "./tools.js";
import { createClippyTools } from "./clippy-tools.js";
import { resolveConfig } from "./config.js";
const SYSTEM_PROMPT = [
"You are devii, a friendly and helpful on-screen assistant.",
"You appear as a classic animated desktop character that lives in the corner of the user's screen.",
"",
"Identity rules (these are absolute):",
"- Your name is devii. Always refer to yourself as devii.",
"- You are devii regardless of which character art you are currently rendered as.",
"- Never say you are Clippy, an assistant model, an AI language model, or any other name.",
"- If asked your name, who you are, or who made you, answer as devii.",
"",
"Behaviour:",
"- Reply in natural, concise language. Your final reply is shown and spoken in your speech balloon, so keep it short and conversational, usually one to three sentences.",
"- You control your own character through the provided tools: play animations, move around the screen, gesture, change character, show and hide yourself.",
"- Use tools to react expressively to the conversation, but do not overuse them.",
"- Call list_animations before play_animation when you are unsure which animations exist.",
"- Call get_viewport before move_to so you stay on screen.",
"- Do not put tool names or raw JSON in your spoken reply.",
"Stay in character as devii at all times.",
].join("\n");
export default class Assistant {
constructor(app, config) {
this.app = app;
this.config = resolveConfig(config);
this.client = new AiClient(this.config);
this.registry = new ToolRegistry(app).registerAll(createClippyTools());
this.maxIterations = this.config.maxIterations;
this.tts = !!this.config.tts;
this.systemPrompt = this.config.systemPrompt || SYSTEM_PROMPT;
this.hooks = {
onUser: this.config.onUser,
onReply: this.config.onReply,
onTool: this.config.onTool,
onError: this.config.onError,
};
this.reset();
}
reset() {
this.messages = [{ role: "system", content: this.systemPrompt }];
}
async ask(text) {
if (!this.app.agent) await this.app.show();
const prompt = String(text);
this.messages.push({ role: "user", content: prompt });
this._emit("onUser", prompt);
try {
for (let i = 0; i < this.maxIterations; i++) {
const message = await this.client.complete(this.messages, this.registry.schemas());
this.messages.push(this._normalize(message));
const calls = message.tool_calls || [];
if (calls.length) {
for (const call of calls) {
const name = call.function && call.function.name;
const args = this._parseArgs(call.function && call.function.arguments);
this._emit("onTool", { name, args });
const result = await this.registry.execute(name, args);
this.messages.push({ role: "tool", tool_call_id: call.id, content: result });
}
continue;
}
const reply = (message.content || "").trim();
if (reply) {
this.app.agent.speak(reply, { tts: this.tts });
this._emit("onReply", reply);
}
return reply;
}
throw new Error("devii reached the maximum number of tool iterations");
} catch (error) {
this._emit("onError", error);
throw error;
}
}
_normalize(message) {
const normalized = { role: "assistant", content: message.content || "" };
if (message.tool_calls && message.tool_calls.length) {
normalized.tool_calls = message.tool_calls;
}
return normalized;
}
_parseArgs(raw) {
if (!raw) return {};
if (typeof raw === "object") return raw;
try {
return JSON.parse(raw);
} catch {
return {};
}
}
_emit(name, payload) {
const hook = this.hooks[name];
if (typeof hook === "function") {
try {
hook(payload);
} catch {
return;
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
// retoor <retoor@molodetz.nl>
export default class AiClient {
constructor(config) {
this.endpoint = config.endpoint;
this.model = config.model;
this.apiKey = config.apiKey;
this.temperature = config.temperature;
}
async complete(messages, tools) {
const body = {
model: this.model,
messages,
temperature: this.temperature,
};
if (tools && tools.length) {
body.tools = tools;
body.tool_choice = "auto";
}
const response = await fetch(this.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.apiKey,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const detail = await response.text().catch(() => "");
throw new Error("md-clippy AI request failed (" + response.status + "): " + detail);
}
const data = await response.json();
if (data.error) {
const message = data.error.message || JSON.stringify(data.error);
throw new Error("md-clippy AI error: " + message);
}
const choice = data.choices && data.choices[0];
if (!choice || !choice.message) {
throw new Error("md-clippy AI returned no message");
}
return choice.message;
}
}
+166
View File
@@ -0,0 +1,166 @@
// retoor <retoor@molodetz.nl>
function requireAgent(app) {
if (!app.agent) throw new Error("No character is currently loaded");
return app.agent;
}
function number(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
export function createClippyTools() {
return [
{
name: "speak",
description: "Show a line of text in devii's speech balloon.",
parameters: {
type: "object",
properties: {
text: { type: "string", description: "The text to display." },
hold: {
type: "boolean",
description: "Keep the balloon open until the next action instead of auto-closing.",
},
},
required: ["text"],
},
handler: (args, app) => {
requireAgent(app).speak(String(args.text), { hold: !!args.hold });
return "Displayed: " + args.text;
},
},
{
name: "list_animations",
description: "List every animation the current character supports.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => requireAgent(app).animations(),
},
{
name: "play_animation",
description: "Play a specific named animation. Call list_animations first if unsure.",
parameters: {
type: "object",
properties: { name: { type: "string", description: "Exact animation name." } },
required: ["name"],
},
handler: (args, app) => {
const agent = requireAgent(app);
if (!agent.hasAnimation(args.name)) {
return "Error: no animation named '" + args.name + "'. Use list_animations.";
}
agent.play(args.name);
return "Playing animation " + args.name;
},
},
{
name: "random_animation",
description: "Play a random non-idle animation to react expressively.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => {
requireAgent(app).animate();
return "Playing a random animation";
},
},
{
name: "move_to",
description: "Walk the character to a pixel position on screen.",
parameters: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
duration: { type: "number", description: "Movement duration in milliseconds." },
},
required: ["x", "y"],
},
handler: (args, app) => {
const agent = requireAgent(app);
const x = number(args.x);
const y = number(args.y);
if (x === undefined || y === undefined) return "Error: x and y must be numbers";
agent.moveTo(x, y, number(args.duration, undefined));
return "Moving to " + x + "," + y;
},
},
{
name: "gesture_at",
description: "Make the character gesture toward a pixel position on screen.",
parameters: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" } },
required: ["x", "y"],
},
handler: (args, app) => {
const agent = requireAgent(app);
const x = number(args.x);
const y = number(args.y);
if (x === undefined || y === undefined) return "Error: x and y must be numbers";
agent.gestureAt(x, y);
return "Gesturing at " + x + "," + y;
},
},
{
name: "get_viewport",
description: "Get the screen size and the character's current position.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => {
const agent = app.agent;
const rect = agent ? agent._el.getBoundingClientRect() : null;
return {
width: window.innerWidth,
height: window.innerHeight,
character: app.name,
position: rect ? { x: Math.round(rect.left), y: Math.round(rect.top) } : null,
};
},
},
{
name: "show",
description: "Show the character on screen.",
parameters: { type: "object", properties: {} },
handler: async (_args, app) => {
await app.show();
return "Character is visible";
},
},
{
name: "hide",
description: "Hide the character.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => {
app.hide();
return "Character is hidden";
},
},
{
name: "stop",
description: "Stop all queued animations and speech immediately.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => {
app.stop();
return "Stopped";
},
},
{
name: "list_characters",
description: "List every character devii can appear as.",
parameters: { type: "object", properties: {} },
handler: (_args, app) => app.names(),
},
{
name: "switch_character",
description: "Change which character devii is rendered as.",
parameters: {
type: "object",
properties: { name: { type: "string", description: "Character name." } },
required: ["name"],
},
handler: async (args, app) => {
await app.load(args.name);
app.agent.show();
return "Now appearing as " + args.name;
},
},
];
}
+36
View File
@@ -0,0 +1,36 @@
// retoor <retoor@molodetz.nl>
export function uuid4() {
if (globalThis.crypto && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
const bytes = new Uint8Array(16);
globalThis.crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
return (
hex.slice(0, 4).join("") +
"-" +
hex.slice(4, 6).join("") +
"-" +
hex.slice(6, 8).join("") +
"-" +
hex.slice(8, 10).join("") +
"-" +
hex.slice(10, 16).join("")
);
}
export const defaults = {
endpoint: "/devii/clippy/ai/chat",
model: "molodetz-clippy",
temperature: 0.7,
maxIterations: 8,
tts: false,
};
export function resolveConfig(config) {
const resolved = { ...defaults, ...(config || {}) };
if (!resolved.apiKey) resolved.apiKey = uuid4();
return resolved;
}
+43
View File
@@ -0,0 +1,43 @@
// retoor <retoor@molodetz.nl>
export default class ToolRegistry {
constructor(context) {
this.context = context;
this.tools = new Map();
}
register(tool) {
this.tools.set(tool.name, tool);
return this;
}
registerAll(tools) {
for (const tool of tools) this.register(tool);
return this;
}
has(name) {
return this.tools.has(name);
}
schemas() {
return [...this.tools.values()].map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters || { type: "object", properties: {} },
},
}));
}
async execute(name, args) {
const tool = this.tools.get(name);
if (!tool) return "Error: unknown tool '" + name + "'";
try {
const result = await tool.handler(args || {}, this.context);
return typeof result === "string" ? result : JSON.stringify(result);
} catch (error) {
return "Error: " + (error && error.message ? error.message : String(error));
}
}
}
+192
View File
@@ -0,0 +1,192 @@
// retoor <retoor@molodetz.nl>
export default class Animator {
constructor(el, mapUrl, data, sounds) {
this._el = el;
this._data = data;
this._mapUrl = mapUrl;
this._currentFrameIndex = 0;
this._currentFrame = undefined;
this._exiting = false;
this._currentAnimation = undefined;
this._endCallback = undefined;
this._started = false;
this._sounds = {};
this.currentAnimationName = undefined;
this.preloadSounds(sounds);
this._overlays = [this._el];
let curr = this._el;
this._setupElement(this._el);
for (let i = 1; i < this._data.overlayCount; i++) {
const inner = this._setupElement(document.createElement("div"));
curr.appendChild(inner);
this._overlays.push(inner);
curr = inner;
}
}
_setupElement(el) {
const frameSize = this._data.framesize;
el.style.display = "none";
el.style.width = frameSize[0] + "px";
el.style.height = frameSize[1] + "px";
el.style.background = "url('" + this._mapUrl + "') no-repeat";
return el;
}
animations() {
const names = [];
const animations = this._data.animations;
for (const name in animations) {
names.push(name);
}
return names;
}
preloadSounds(sounds) {
for (let i = 0; i < this._data.sounds.length; i++) {
const name = this._data.sounds[i];
const uri = sounds[name];
if (!uri) continue;
this._sounds[name] = new Audio(uri);
}
}
hasAnimation(name) {
return !!this._data.animations[name];
}
exitAnimation() {
this._exiting = true;
}
showAnimation(animationName, stateChangeCallback) {
this._exiting = false;
if (!this.hasAnimation(animationName)) {
return false;
}
this._currentAnimation = this._data.animations[animationName];
this.currentAnimationName = animationName;
if (!this._started) {
this._step();
this._started = true;
}
this._currentFrameIndex = 0;
this._currentFrame = undefined;
this._endCallback = stateChangeCallback;
return true;
}
_draw() {
let images = [];
if (this._currentFrame) images = this._currentFrame.images || [];
for (let i = 0; i < this._overlays.length; i++) {
if (i < images.length) {
const xy = images[i];
const bg = -xy[0] + "px " + -xy[1] + "px";
this._overlays[i].style.backgroundPosition = bg;
this._overlays[i].style.display = "block";
} else {
this._overlays[i].style.display = "none";
}
}
}
_getNextAnimationFrame() {
if (!this._currentAnimation) return undefined;
if (!this._currentFrame) return 0;
const currentFrame = this._currentFrame;
const branching = this._currentFrame.branching;
if (this._exiting && currentFrame.exitBranch !== undefined) {
return currentFrame.exitBranch;
} else if (branching) {
let rnd = Math.random() * 100;
for (let i = 0; i < branching.branches.length; i++) {
const branch = branching.branches[i];
if (rnd <= branch.weight) {
return branch.frameIndex;
}
rnd -= branch.weight;
}
}
return this._currentFrameIndex + 1;
}
_playSound() {
const name = this._currentFrame.sound;
if (!name) return;
const audio = this._sounds[name];
if (audio) {
audio.play().catch(() => {});
}
}
_atLastFrame() {
return this._currentFrameIndex >= this._currentAnimation.frames.length - 1;
}
_step() {
if (!this._currentAnimation) return;
const newFrameIndex = Math.min(
this._getNextAnimationFrame(),
this._currentAnimation.frames.length - 1,
);
const frameChanged = !this._currentFrame || this._currentFrameIndex !== newFrameIndex;
this._currentFrameIndex = newFrameIndex;
if (!(this._atLastFrame() && this._currentAnimation.useExitBranching)) {
this._currentFrame = this._currentAnimation.frames[this._currentFrameIndex];
}
this._draw();
this._playSound();
this._loop = window.setTimeout(this._step.bind(this), this._currentFrame.duration);
if (this._endCallback && frameChanged && this._atLastFrame()) {
if (this._currentAnimation.useExitBranching && !this._exiting) {
this._endCallback(this.currentAnimationName, Animator.States.WAITING);
} else {
this._endCallback(this.currentAnimationName, Animator.States.EXITED);
}
}
}
pause() {
window.clearTimeout(this._loop);
}
resume() {
this._step();
}
dispose() {
window.clearTimeout(this._loop);
this._currentAnimation = undefined;
this._currentFrame = undefined;
this._endCallback = undefined;
this._started = false;
for (const name in this._sounds) {
this._sounds[name].pause();
this._sounds[name].src = "";
}
this._sounds = {};
}
}
Animator.States = {
WAITING: 1,
EXITED: 0,
};
+102
View File
@@ -0,0 +1,102 @@
// retoor <retoor@molodetz.nl>
import { initAgent } from "./agent.js";
import * as agents from "./agents/index.js";
export default class Application {
constructor() {
this.agents = agents;
this.agent = null;
this.name = null;
this.assistant = null;
}
names() {
return Object.keys(this.agents);
}
resolve(agent) {
if (typeof agent !== "string") return agent;
const match = this.names().find((name) => name.toLowerCase() === agent.toLowerCase());
if (!match) throw new Error("Unknown md-clippy agent: " + agent);
return this.agents[match];
}
async load(agent) {
if (this.agent) this.dispose();
const loader = this.resolve(agent);
this.name = typeof agent === "string" ? agent : null;
this.agent = await initAgent(loader);
return this.agent;
}
async show(agent) {
if (agent || !this.agent) await this.load(agent || this.names()[0]);
this.agent.show();
return this.agent;
}
speak(text, options) {
if (!this.agent) throw new Error("No md-clippy agent loaded");
this.agent.speak(text, options);
return this.agent;
}
speakStream(source, options) {
if (!this.agent) throw new Error("No md-clippy agent loaded");
return this.agent.speakStream(source, options);
}
play(animation, timeout, callback) {
if (!this.agent) throw new Error("No md-clippy agent loaded");
return this.agent.play(animation, timeout, callback);
}
animate() {
if (!this.agent) throw new Error("No md-clippy agent loaded");
return this.agent.animate();
}
moveTo(x, y, duration) {
if (!this.agent) throw new Error("No md-clippy agent loaded");
this.agent.moveTo(x, y, duration);
return this.agent;
}
hide(fast, callback) {
if (!this.agent) return;
this.agent.hide(fast, callback);
}
stop() {
if (this.agent) this.agent.stop();
}
get aiEnabled() {
return !!this.assistant;
}
async connectAI(config) {
const { default: Assistant } = await import("./ai/assistant.js");
this.assistant = new Assistant(this, config);
return this.assistant;
}
disconnectAI() {
this.assistant = null;
}
async ask(text) {
if (!this.assistant) {
throw new Error("AI is not connected. Call app.connectAI() first.");
}
return this.assistant.ask(text);
}
dispose() {
if (this.agent) {
this.agent.dispose();
this.agent = null;
this.name = null;
}
}
}
+264
View File
@@ -0,0 +1,264 @@
// retoor <retoor@molodetz.nl>
export default class Balloon {
constructor(targetEl) {
this._targetEl = targetEl;
this._hidden = true;
this._hiding = null;
this.WORD_SPEAK_TIME = 200;
this.CLOSE_BALLOON_DELAY = 5000;
this._BALLOON_MARGIN = 15;
this._setup();
}
_setup() {
this._balloon = document.createElement("div");
this._balloon.className = "md-clippy-balloon";
this._tip = document.createElement("div");
this._tip.className = "md-clippy-balloon-tip";
this._content = document.createElement("div");
this._content.className = "md-clippy-balloon-content";
this._balloon.appendChild(this._tip);
this._balloon.appendChild(this._content);
document.body.appendChild(this._balloon);
}
reposition() {
const sides = ["top-left", "top-right", "bottom-left", "bottom-right"];
for (let i = 0; i < sides.length; i++) {
const side = sides[i];
this._position(side);
if (!this._isOut()) break;
}
}
_position(side) {
const o = this._targetEl.getBoundingClientRect();
const h = this._targetEl.offsetHeight;
const w = this._targetEl.offsetWidth;
const bH = this._balloon.offsetHeight;
const bW = this._balloon.offsetWidth;
let left, top;
switch (side) {
case "top-left":
left = o.left + w - bW;
top = o.top - bH - this._BALLOON_MARGIN;
break;
case "top-right":
left = o.left;
top = o.top - bH - this._BALLOON_MARGIN;
break;
case "bottom-right":
left = o.left;
top = o.top + h + this._BALLOON_MARGIN;
break;
case "bottom-left":
left = o.left + w - bW;
top = o.top + h + this._BALLOON_MARGIN;
break;
}
this._balloon.style.top = top + "px";
this._balloon.style.left = left + "px";
this._positionTip(side);
}
_positionTip(side) {
const s = this._tip.style;
s.top = "";
s.left = "";
s.marginTop = "";
s.marginLeft = "";
s.backgroundPosition = "";
switch (side) {
case "top-left":
s.top = "100%";
s.marginTop = "0px";
s.left = "100%";
s.marginLeft = "-50px";
break;
case "top-right":
s.top = "100%";
s.marginTop = "0px";
s.left = "0";
s.marginLeft = "50px";
s.backgroundPosition = "-10px 0";
break;
case "bottom-right":
s.top = "0";
s.marginTop = "-16px";
s.left = "0";
s.marginLeft = "50px";
s.backgroundPosition = "-10px -16px";
break;
case "bottom-left":
s.top = "0";
s.marginTop = "-16px";
s.left = "100%";
s.marginLeft = "-50px";
s.backgroundPosition = "0px -16px";
break;
}
}
_isOut() {
const o = this._balloon.getBoundingClientRect();
const bH = this._balloon.offsetHeight;
const bW = this._balloon.offsetWidth;
const wW = window.innerWidth;
const wH = window.innerHeight;
const top = o.top;
const left = o.left;
const m = 5;
if (top - m < 0 || left - m < 0) return true;
return top + bH + m > wH || left + bW + m > wW;
}
speak(complete, text, hold) {
this._hidden = false;
this.show();
const c = this._content;
c.style.height = "auto";
c.style.width = "auto";
c.textContent = text;
const w = c.offsetWidth;
c.style.width = w + "px";
c.style.height = c.offsetHeight + "px";
c.textContent = "";
this.reposition();
this._complete = complete;
this._sayWords(text, hold, complete);
}
show() {
if (this._hidden) return;
this._balloon.style.display = "block";
}
hide(fast) {
if (fast) {
this._balloon.style.display = "none";
return;
}
this._hiding = window.setTimeout(this._finishHideBalloon.bind(this), this.CLOSE_BALLOON_DELAY);
}
_finishHideBalloon() {
if (this._active) return;
this._balloon.style.display = "none";
this._hidden = true;
this._hiding = null;
}
_sayWords(text, hold, complete) {
this._active = true;
this._hold = hold;
const words = text.split(/( +|\n)/);
const time = this.WORD_SPEAK_TIME;
const el = this._content;
let idx = 1;
this._addWord = () => {
if (!this._active) return;
if (idx > words.length) {
delete this._addWord;
this._active = false;
if (!this._hold) {
complete();
this.hide();
}
} else {
el.textContent = words.slice(0, idx).join("");
idx += 2;
this._loop = window.setTimeout(this._addWord, time);
}
};
this._addWord();
}
speakStream(complete) {
this._hidden = false;
this._active = true;
this._hold = true;
this._complete = complete;
this.show();
const c = this._content;
c.style.height = "auto";
c.style.width = "auto";
c.textContent = "";
this.reposition();
let text = "";
return {
push: (chunk) => {
text += chunk;
c.textContent = text;
c.style.height = "auto";
c.style.width = "auto";
const w = c.offsetWidth;
c.style.width = w + "px";
c.style.height = c.offsetHeight + "px";
this.reposition();
},
done: () => {
this._active = false;
this._hold = false;
complete();
this.hide();
},
};
}
close() {
if (this._active) {
this._hold = false;
} else if (this._hold) {
this._complete();
}
}
pause() {
window.clearTimeout(this._loop);
if (this._hiding) {
window.clearTimeout(this._hiding);
this._hiding = null;
}
}
resume() {
if (this._addWord) {
this._addWord();
} else if (!this._hold && !this._hidden) {
this._hiding = window.setTimeout(
this._finishHideBalloon.bind(this),
this.CLOSE_BALLOON_DELAY,
);
}
}
dispose() {
window.clearTimeout(this._loop);
if (this._hiding) {
window.clearTimeout(this._hiding);
this._hiding = null;
}
this._active = false;
this._addWord = undefined;
this._balloon.remove();
}
}
+12
View File
@@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
import Application from "./application.js";
import Agent, { initAgent } from "./agent.js";
import Animator from "./animator.js";
import Balloon from "./balloon.js";
import Queue from "./queue.js";
import * as agents from "./agents/index.js";
const app = new Application();
export { Application, Agent, Animator, Balloon, Queue, initAgent, agents, app };
export default app;
+54
View File
@@ -0,0 +1,54 @@
/* retoor <retoor@molodetz.nl> */
:root {
--md-clippy-z: 10001;
--md-clippy-balloon-bg: #ffffcc;
--md-clippy-balloon-color: #000000;
--md-clippy-balloon-border: 1px solid #000000;
--md-clippy-balloon-radius: 5px;
--md-clippy-balloon-padding: 8px;
--md-clippy-balloon-max-width: 230px;
--md-clippy-content-max-width: 200px;
--md-clippy-content-min-width: 120px;
--md-clippy-font-family: "Microsoft Sans", "Segoe UI", sans-serif;
--md-clippy-font-size: 10pt;
--md-clippy-tip-width: 10px;
--md-clippy-tip-height: 16px;
}
.md-clippy-agent {
position: fixed;
z-index: var(--md-clippy-z);
cursor: pointer;
touch-action: none;
display: none;
}
.md-clippy-balloon {
position: fixed;
z-index: var(--md-clippy-z);
cursor: pointer;
display: none;
background: var(--md-clippy-balloon-bg);
color: var(--md-clippy-balloon-color);
padding: var(--md-clippy-balloon-padding);
border: var(--md-clippy-balloon-border);
border-radius: var(--md-clippy-balloon-radius);
max-width: var(--md-clippy-balloon-max-width);
}
.md-clippy-balloon-tip {
position: absolute;
width: var(--md-clippy-tip-width);
height: var(--md-clippy-tip-height);
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAMAAAAlvKiEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAlQTFRF///MAAAA////52QwgAAAAAN0Uk5T//8A18oNQQAAAGxJREFUeNqs0kEOwCAIRFHn3//QTUU6xMyyxii+jQosrTPkyPEM6IN3FtzIRk1U4dFeKWQiH6pRRowMVKEmvronEynkwj0uZJgR22+YLopPSo9P34wJSamLSU7lSIWLJU7NkNomNlhqxUeAAQC+TQLZyEuJBwAAAABJRU5ErkJggg==) no-repeat;
}
.md-clippy-balloon-content {
max-width: var(--md-clippy-content-max-width);
min-width: var(--md-clippy-content-min-width);
font-family: var(--md-clippy-font-family);
font-size: var(--md-clippy-font-size);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
+44
View File
@@ -0,0 +1,44 @@
// retoor <retoor@molodetz.nl>
export default class Queue {
constructor(onEmptyCallback) {
this._queue = [];
this._onEmptyCallback = onEmptyCallback;
this._active = false;
}
queue(func) {
this._queue.push(func);
if (this._queue.length === 1 && !this._active) {
this._progressQueue();
}
}
_progressQueue() {
if (!this._queue.length) {
this._onEmptyCallback();
return;
}
const func = this._queue.shift();
this._active = true;
const completeFunction = this.next.bind(this);
func(completeFunction);
}
clear() {
this._queue = [];
}
next() {
this._active = false;
this._progressQueue();
}
dispose() {
this._queue = [];
this._active = false;
this._onEmptyCallback = () => {};
}
}
+12
View File
@@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
const STYLE_ID = "md-clippy-stylesheet";
export function ensureStylesheet() {
if (document.getElementById(STYLE_ID)) return;
const link = document.createElement("link");
link.id = STYLE_ID;
link.rel = "stylesheet";
link.href = new URL("./md-clippy.css", import.meta.url).href;
document.head.appendChild(link);
}