generated from ztimson/template
All checks were successful
Build and publish / Build Container (push) Successful in 1m41s
255 lines
6.8 KiB
JavaScript
255 lines
6.8 KiB
JavaScript
export default class Navi {
|
|
api;
|
|
connected = false;
|
|
icon;
|
|
info;
|
|
theme;
|
|
world;
|
|
|
|
#init;
|
|
#listeners = new Map();
|
|
#socket;
|
|
#secret;
|
|
#world;
|
|
|
|
constructor(api = window.location.origin, secret = '') {
|
|
this.api = api.replace(/\/$/, '');
|
|
this.icon = `${this.api}/favicon.png`;
|
|
this.#secret = secret;
|
|
}
|
|
|
|
async init() {
|
|
if(this.#init) return this.#init;
|
|
this.#init = new Promise(async (res, rej) => {
|
|
try {
|
|
this.info = await fetch(`${this.api}/api/info`, {
|
|
headers: this.#secret ? {'Authorization': `Bearer ${this.#secret}`} : {}
|
|
}).then(resp => {
|
|
if(!resp.ok) throw new Error(`Invalid Navi API: ${this.api}`);
|
|
return resp.json();
|
|
});
|
|
this.theme = this.info.theme;
|
|
|
|
this.#socket = io(this.api, {auth: this.#secret ? {token: this.#secret} : null});
|
|
|
|
this.#socket.on('llm-stream', (chunk) => {
|
|
if(this.llmCallback) this.llmCallback(chunk);
|
|
this.emit('llm:stream', chunk);
|
|
});
|
|
|
|
this.#socket.on('llm-response', (data) => {
|
|
if(this.llmResolve) this.llmResolve(data);
|
|
this.emit('llm:response', data);
|
|
this.llmCallback = null;
|
|
this.llmResolve = null;
|
|
this.llmReject = null;
|
|
});
|
|
|
|
this.#socket.on('llm-error', (error) => {
|
|
if(this.llmReject) this.llmReject(error);
|
|
this.emit('llm:error', error);
|
|
this.llmCallback = null;
|
|
this.llmResolve = null;
|
|
this.llmReject = null;
|
|
});
|
|
|
|
this.connected = true;
|
|
this.emit('init');
|
|
res(this);
|
|
} catch (err) {
|
|
rej(err);
|
|
}
|
|
});
|
|
return this.#init;
|
|
}
|
|
|
|
// ============================================
|
|
// EVENT SYSTEM
|
|
// ============================================
|
|
|
|
on(event, callback) {
|
|
if(!this.#listeners.has(event)) this.#listeners.set(event, []);
|
|
this.#listeners.get(event).push(callback);
|
|
return () => this.off(event, callback);
|
|
}
|
|
|
|
off(event, callback) {
|
|
const callbacks = this.#listeners.get(event);
|
|
if(callbacks) {
|
|
const index = callbacks.indexOf(callback);
|
|
if(index > -1) callbacks.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
emit(event, data) {
|
|
const callbacks = this.#listeners.get(event) || [];
|
|
callbacks.forEach(cb => cb(data));
|
|
}
|
|
|
|
// ============================================
|
|
// REST API
|
|
// ============================================
|
|
|
|
async sendUserMessage(userId, message) {
|
|
const response = await fetch(`${this.api}/api/message`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message })
|
|
});
|
|
if (!response.ok) throw new Error('Message failed to send bestie');
|
|
|
|
const result = await response.json();
|
|
this.emit('message:sent', { userId, message, result });
|
|
return result;
|
|
}
|
|
|
|
async linkPet(petId, targetApiUrl) {
|
|
const response = await fetch(`${this.api}/api/link`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ targetApiUrl })
|
|
});
|
|
if (!response.ok) throw new Error('Link connection is bussin (negatively)');
|
|
|
|
const result = await response.json();
|
|
this.emit('pet:linked', { petId, targetApiUrl, result });
|
|
return result;
|
|
}
|
|
|
|
// ============================================
|
|
// WORLD SOCKET
|
|
// ============================================
|
|
|
|
connect(apiOrWorld, world) {
|
|
let api;
|
|
if(world) {
|
|
api = apiOrWorld;
|
|
} else {
|
|
api = this.api;
|
|
world = apiOrWorld;
|
|
}
|
|
|
|
if(!this.world) this.world = {};
|
|
if(this.#world && this.world.api !== api) {
|
|
this.#world.disconnect();
|
|
this.#world = null;
|
|
}
|
|
if(!this.#world) this.#world = io(`${api}/world`, {auth: this.#secret ? {token: this.#secret} : null});
|
|
this.world.api = api;
|
|
this.world.data = null;
|
|
this.world.name = world;
|
|
this.world.players = new Map();
|
|
|
|
const callbacks = {
|
|
move: (x, y) => {
|
|
this.#world.emit('move', {x, y});
|
|
},
|
|
leave: () => {
|
|
this.#world.disconnect();
|
|
this.world = null;
|
|
},
|
|
onData: (data) => { },
|
|
onPlayers: (players) => { },
|
|
onJoined: (player) => { },
|
|
onMoved: (player) => { },
|
|
onLeft: (player) => { },
|
|
onError: (error) => { }
|
|
}
|
|
|
|
this.#world.on('data', (data) => {
|
|
this.world.data = data;
|
|
callbacks.onData(data);
|
|
this.emit('world:data', data);
|
|
});
|
|
|
|
this.#world.on('players', (players) => {
|
|
this.world.players.clear();
|
|
players.forEach(p => this.world.players.set(p.socketId, p));
|
|
callbacks.onPlayers(players);
|
|
this.emit('world:players', players)
|
|
});
|
|
|
|
this.#world.on('joined', (player) => {
|
|
this.world.players.set(player.socketId, player);
|
|
callbacks.onJoined(player);
|
|
this.emit('world:joined', player);
|
|
});
|
|
|
|
this.#world.on('moved', (data) => {
|
|
const player = this.world.players.get(data.socketId);
|
|
if(player) {
|
|
player.x = data.x;
|
|
player.y = data.y;
|
|
}
|
|
callbacks.onMoved(player);
|
|
this.emit('world:moved', player);
|
|
});
|
|
|
|
this.#world.on('left', (data) => {
|
|
const player = this.world.players.get(data.socketId);
|
|
this.world.players.delete(data.socketId);
|
|
callbacks.onLeft(player);
|
|
this.emit('world:left', player);
|
|
});
|
|
|
|
this.#world.on('error', (error) => {
|
|
console.error('World error:', error);
|
|
callbacks.onError(error);
|
|
this.emit('world:error', error);
|
|
});
|
|
|
|
this.#world.emit('join', {world, api});
|
|
|
|
this.emit('world:join', world);
|
|
return callbacks;
|
|
}
|
|
|
|
// ============================================
|
|
// LLM
|
|
// ============================================
|
|
|
|
ask(message, stream) {
|
|
this.llmCallback = stream;
|
|
const promise = new Promise((resolve, reject) => {
|
|
this.llmResolve = resolve;
|
|
this.llmReject = reject;
|
|
this.#socket.emit('llm-ask', {message});
|
|
this.emit('llm:ask')
|
|
});
|
|
|
|
promise.abort = () => {
|
|
this.#socket.emit('llm-abort');
|
|
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
|
|
this.llmCallback = null;
|
|
this.llmResolve = null;
|
|
this.llmReject = null;
|
|
this.emit('llm:abort')
|
|
};
|
|
|
|
return promise;
|
|
}
|
|
|
|
clearChat() {
|
|
if(this.#socket) this.#socket.emit('llm-clear');
|
|
this.emit('llm:clear');
|
|
}
|
|
|
|
// ============================================
|
|
// UTILITY
|
|
// ============================================
|
|
|
|
disconnect() {
|
|
this.connected = false;
|
|
this.icon = this.info = this.theme = this.world = this.#init = this.#secret = null;
|
|
if(this.#world) {
|
|
this.#world.disconnect();
|
|
this.#world = null;
|
|
}
|
|
if(this.#socket) {
|
|
this.#socket.disconnect();
|
|
this.#socket = null;
|
|
}
|
|
this.emit('disconnected');
|
|
}
|
|
}
|