Styling improvment
All checks were successful
Build and publish / Build Container (push) Successful in 1m41s

This commit is contained in:
2026-03-02 02:49:15 -05:00
parent c5c070ebc2
commit 8c2b80951b
15 changed files with 824 additions and 823 deletions

254
public/navi.mjs Normal file
View File

@@ -0,0 +1,254 @@
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');
}
}