Refactored code formatting to use consistent indentation and object destructuring across client and server files.
All checks were successful
Build and publish / Build Container (push) Successful in 1m44s

This commit is contained in:
2026-03-02 12:28:18 -05:00
parent 09a59f170c
commit 7b2621c264
5 changed files with 627 additions and 519 deletions

View File

@@ -13,8 +13,10 @@ function shadeColor(hex, amount) {
} }
function hex2Int(hex) { function hex2Int(hex) {
let r = 0, g = 0, b = 0; let r = 0,
if (hex.length === 4) { g = 0,
b = 0;
if(hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16); r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16); g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16); b = parseInt(hex[3] + hex[3], 16);
@@ -23,15 +25,19 @@ function shadeColor(hex, amount) {
g = parseInt(hex.slice(3, 5), 16); g = parseInt(hex.slice(3, 5), 16);
b = parseInt(hex.slice(5, 7), 16); b = parseInt(hex.slice(5, 7), 16);
} }
return { r, g, b }; return {
r,
g,
b
};
} }
function hue2rgb(p, q, t) { function hue2rgb(p, q, t) {
if (t < 0) t += 1; if(t < 0) t += 1;
if (t > 1) t -= 1; if(t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t; if(t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q; if(t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p; return p;
} }
@@ -39,21 +45,38 @@ function shadeColor(hex, amount) {
return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b); return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b);
} }
let { r, g, b } = hex2Int(hex); let {
r /= 255; g /= 255; b /= 255; r,
const max = Math.max(r, g, b), min = Math.min(r, g, b); g,
let h, s, l = (max + min) / 2; b
} = hex2Int(hex);
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) { if(max === min) {
h = s = 0; h = s = 0;
} else { } else {
const d = max - min; const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break; case r:
case g: h = (b - r) / d + 2; break; h = (g - b) / d + (g < b ? 6 : 0);
case b: h = (r - g) / d + 4; break; break;
default: h = 0; break; case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
h = 0;
break;
} }
h /= 6; h /= 6;
} }
@@ -67,7 +90,7 @@ function shadeColor(hex, amount) {
class BtnComponent extends HTMLElement { class BtnComponent extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>

View File

@@ -11,7 +11,7 @@ const TILE_DEPTH = 16;
function applyTheme(theme) { function applyTheme(theme) {
const body = document.body; const body = document.body;
if (theme.background.image) { if(theme.background.image) {
body.style.backgroundImage = `url(${theme.background.image})`; body.style.backgroundImage = `url(${theme.background.image})`;
} }
@@ -202,23 +202,23 @@ class Game {
this.world = data; this.world = data;
applyTheme(this.world.theme); applyTheme(this.world.theme);
this.initializeRenderer(); this.initializeRenderer();
} };
this.worldActions.onPlayers = (players) => { this.worldActions.onPlayers = (players) => {
players.forEach(player => { players.forEach(player => {
if (player.name !== this.navi.info.name) { if(player.name !== this.navi.info.name) {
this.addOtherPlayer(player); this.addOtherPlayer(player);
} }
}); });
} };
this.worldActions.onJoined = (player) => { this.worldActions.onJoined = (player) => {
this.addOtherPlayer(player); this.addOtherPlayer(player);
} };
this.worldActions.onMoved = (data) => { this.worldActions.onMoved = (data) => {
const sprite = this.otherPlayers.get(data.socketId); const sprite = this.otherPlayers.get(data.socketId);
if (sprite) { if(sprite) {
this.moveOtherPlayer(sprite, data.x, data.y); this.moveOtherPlayer(sprite, data.x, data.y);
} }
}; };
@@ -226,11 +226,11 @@ class Game {
this.worldActions.onLeft = (data) => { this.worldActions.onLeft = (data) => {
const sprite = this.otherPlayers.get(data.socketId); const sprite = this.otherPlayers.get(data.socketId);
if(sprite) this.otherPlayers.delete(data.socketId); if(sprite) this.otherPlayers.delete(data.socketId);
} };
this.worldActions.onError = (error) => console.error('❌ World error:', error); this.worldActions.onError = (error) => console.error('❌ World error:', error);
console.log('✨ Game initializing...'); console.log('✨ Game initializing...');
} catch (error) { } catch(error) {
console.error('❌ Failed to initialize game:', error); console.error('❌ Failed to initialize game:', error);
} }
} }
@@ -251,7 +251,7 @@ class Game {
const animate = () => { const animate = () => {
progress += 0.08; progress += 0.08;
if (progress >= 1) { if(progress >= 1) {
sprite.x = targetPos.x; sprite.x = targetPos.x;
sprite.y = targetPos.y; sprite.y = targetPos.y;
sprite.gridX = targetX; sprite.gridX = targetX;
@@ -299,9 +299,7 @@ class Game {
} }
movePetTo(targetX, targetY) { movePetTo(targetX, targetY) {
if (this.isMoving || if(this.isMoving || targetX < 0 || targetX >= this.world.gridSize || targetY < 0 || targetY >= this.world.gridSize) return;
targetX < 0 || targetX >= this.world.gridSize ||
targetY < 0 || targetY >= this.world.gridSize) return;
this.isMoving = true; this.isMoving = true;
const targetPos = isoToScreen(targetX, targetY); const targetPos = isoToScreen(targetX, targetY);
@@ -312,7 +310,7 @@ class Game {
const animate = () => { const animate = () => {
progress += 0.08; progress += 0.08;
if (progress >= 1) { if(progress >= 1) {
this.pet.x = targetPos.x; this.pet.x = targetPos.x;
this.pet.y = targetPos.y; this.pet.y = targetPos.y;
this.pet.gridX = targetX; this.pet.gridX = targetX;
@@ -320,7 +318,7 @@ class Game {
this.isMoving = false; this.isMoving = false;
// Use API action to send move 📤 // Use API action to send move 📤
if (this.worldActions) { if(this.worldActions) {
this.worldActions.move(targetX, targetY); this.worldActions.move(targetX, targetY);
} }
return; return;
@@ -337,21 +335,21 @@ class Game {
setupInput() { setupInput() {
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
if (this.dialogue.isOpen) return; if(this.dialogue.isOpen) return;
this.keys[e.key.toLowerCase()] = true; this.keys[e.key.toLowerCase()] = true;
if (!this.isMoving) { if(!this.isMoving) {
let newX = this.pet.gridX; let newX = this.pet.gridX;
let newY = this.pet.gridY; let newY = this.pet.gridY;
if (this.keys['w'] || this.keys['arrowup']) { if(this.keys['w'] || this.keys['arrowup']) {
newY--; newY--;
} else if (this.keys['s'] || this.keys['arrowdown']) { } else if(this.keys['s'] || this.keys['arrowdown']) {
newY++; newY++;
} else if (this.keys['a'] || this.keys['arrowleft']) { } else if(this.keys['a'] || this.keys['arrowleft']) {
newX--; newX--;
} else if (this.keys['d'] || this.keys['arrowright']) { } else if(this.keys['d'] || this.keys['arrowright']) {
newX++; newX++;
} }
@@ -365,7 +363,7 @@ class Game {
} }
gameLoop() { gameLoop() {
if (!this.isMoving && this.pet) { if(!this.isMoving && this.pet) {
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2; this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
} }
} }

View File

@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<title>NetNavi v1.0.0</title> <title>NetNavi v1.0.0</title>
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png"/>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
@@ -50,6 +50,7 @@
<script type="module"> <script type="module">
import Navi from './navi.mjs'; import Navi from './navi.mjs';
window.navi = new Navi(); window.navi = new Navi();
</script> </script>
<script type="module" src="/components/jukebox.mjs"></script> <script type="module" src="/components/jukebox.mjs"></script>

View File

@@ -1,4 +1,4 @@
export default class Navi { class Navi {
api; api;
connected = false; connected = false;
icon; icon;
@@ -56,7 +56,7 @@ export default class Navi {
this.connected = true; this.connected = true;
this.emit('init'); this.emit('init');
res(this); res(this);
} catch (err) { } catch(err) {
rej(err); rej(err);
} }
}); });
@@ -93,26 +93,34 @@ export default class Navi {
async sendUserMessage(userId, message) { async sendUserMessage(userId, message) {
const response = await fetch(`${this.api}/api/message`, { const response = await fetch(`${this.api}/api/message`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ message }) body: JSON.stringify({message})
}); });
if (!response.ok) throw new Error('Message failed to send bestie'); if(!response.ok) throw new Error('Message failed to send bestie');
const result = await response.json(); const result = await response.json();
this.emit('message:sent', { userId, message, result }); this.emit('message:sent', {
userId,
message,
result
});
return result; return result;
} }
async linkPet(petId, targetApiUrl) { async linkPet(petId, targetApiUrl) {
const response = await fetch(`${this.api}/api/link`, { const response = await fetch(`${this.api}/api/link`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ targetApiUrl }) body: JSON.stringify({targetApiUrl})
}); });
if (!response.ok) throw new Error('Link connection is bussin (negatively)'); if(!response.ok) throw new Error('Link connection is bussin (negatively)');
const result = await response.json(); const result = await response.json();
this.emit('pet:linked', { petId, targetApiUrl, result }); this.emit('pet:linked', {
petId,
targetApiUrl,
result
});
return result; return result;
} }
@@ -142,19 +150,28 @@ export default class Navi {
const callbacks = { const callbacks = {
move: (x, y) => { move: (x, y) => {
this.#world.emit('move', {x, y}); this.#world.emit('move', {
x,
y
});
}, },
leave: () => { leave: () => {
this.#world.disconnect(); this.#world.disconnect();
this.world = null; this.world = null;
}, },
onData: (data) => { }, onData: (data) => {
onPlayers: (players) => { }, },
onJoined: (player) => { }, onPlayers: (players) => {
onMoved: (player) => { }, },
onLeft: (player) => { }, onJoined: (player) => {
onError: (error) => { } },
onMoved: (player) => {
},
onLeft: (player) => {
},
onError: (error) => {
} }
};
this.#world.on('data', (data) => { this.#world.on('data', (data) => {
this.world.data = data; this.world.data = data;
@@ -166,7 +183,7 @@ export default class Navi {
this.world.players.clear(); this.world.players.clear();
players.forEach(p => this.world.players.set(p.socketId, p)); players.forEach(p => this.world.players.set(p.socketId, p));
callbacks.onPlayers(players); callbacks.onPlayers(players);
this.emit('world:players', players) this.emit('world:players', players);
}); });
this.#world.on('joined', (player) => { this.#world.on('joined', (player) => {
@@ -198,7 +215,10 @@ export default class Navi {
this.emit('world:error', error); this.emit('world:error', error);
}); });
this.#world.emit('join', {world, api}); this.#world.emit('join', {
world,
api
});
this.emit('world:join', world); this.emit('world:join', world);
return callbacks; return callbacks;
@@ -214,7 +234,7 @@ export default class Navi {
this.llmResolve = resolve; this.llmResolve = resolve;
this.llmReject = reject; this.llmReject = reject;
this.#socket.emit('llm-ask', {message}); this.#socket.emit('llm-ask', {message});
this.emit('llm:ask') this.emit('llm:ask');
}); });
promise.abort = () => { promise.abort = () => {
@@ -223,7 +243,7 @@ export default class Navi {
this.llmCallback = null; this.llmCallback = null;
this.llmResolve = null; this.llmResolve = null;
this.llmReject = null; this.llmReject = null;
this.emit('llm:abort') this.emit('llm:abort');
}; };
return promise; return promise;
@@ -252,3 +272,5 @@ export default class Navi {
this.emit('disconnected'); this.emit('disconnected');
} }
} }
export default Navi;

View File

@@ -1,12 +1,34 @@
import express from 'express'; import express
import { createServer } from 'http'; from 'express';
import { Server } from 'socket.io'; import {
import cors from 'cors'; createServer
import fs from 'fs'; } from 'http';
import { join, dirname } from 'path'; import {
import { fileURLToPath } from 'url'; Server
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils'; } from 'socket.io';
import {contrast, shadeColor} from '@ztimson/utils'; import cors
from 'cors';
import fs
from 'fs';
import {
join,
dirname
} from 'path';
import {
fileURLToPath
} from 'url';
import {
Ai,
DateTimeTool,
ExecTool,
FetchTool,
ReadWebpageTool,
WebSearchTool
} from '@ztimson/ai-utils';
import {
contrast,
shadeColor
} from '@ztimson/utils';
// ============================================ // ============================================
// Settings // Settings
@@ -38,10 +60,11 @@ function calcColors(theme) {
mutedContrast: contrast(theme.muted), mutedContrast: contrast(theme.muted),
mutedDark: shadeColor(theme.muted, -.1), mutedDark: shadeColor(theme.muted, -.1),
mutedLight: shadeColor(theme.muted, .1), mutedLight: shadeColor(theme.muted, .1),
} };
} }
let memories = [], settings = { let memories = [],
settings = {
name: 'Navi', name: 'Navi',
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them', personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
instructions: '', instructions: '',
@@ -53,7 +76,7 @@ let memories = [], settings = {
accent: '#6f16c3', accent: '#6f16c3',
muted: '#a8a8a8', muted: '#a8a8a8',
} }
}; };
function load() { function load() {
try { try {
@@ -72,9 +95,9 @@ function save() {
updated = false; updated = false;
const dir = dirname(settingsFile); const dir = dirname(settingsFile);
if (!fs.existsSync(dir)) { if(!fs.existsSync(dir)) {
fs.mkdir(dir, { recursive: true }, (err) => { fs.mkdir(dir, {recursive: true}, (err) => {
if (err) throw err; // Fail loudly if dirs cant be made 💀 if(err) throw err; // Fail loudly if dirs cant be made 💀
}); });
} }
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2)); fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
@@ -85,13 +108,22 @@ load();
const ai = new Ai({ const ai = new Ai({
llm: { llm: {
models: { models: {
'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'}, 'Ministral-3': {
proto: 'openai',
host: 'http://10.69.0.55:11728',
token: 'ignore'
}, },
system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. You have your own debian environment you can access using the exec tool with the cli language. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`, },
system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. You have your own debian environment you can access using the exec tool with the cli language, use /tmp as your working directory. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`,
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, { tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
name: 'adjust_personality', name: 'adjust_personality',
description: 'Replace your current personality instructions', description: 'Replace your current personality instructions',
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}}, args: {
instructions: {
type: 'string',
description: 'Bullet point list of how to behave'
}
},
fn: (args) => { fn: (args) => {
settings.personality = args.instructions; settings.personality = args.instructions;
updated = true; updated = true;
@@ -108,7 +140,10 @@ const ai = new Ai({
const app = express(); const app = express();
const httpServer = createServer(app); const httpServer = createServer(app);
const io = new Server(httpServer, { const io = new Server(httpServer, {
cors: {origin: "*", methods: ["GET", "POST"]} cors: {
origin: '*',
methods: ['GET', 'POST']
}
}); });
app.use(cors()); app.use(cors());
@@ -131,14 +166,14 @@ io.on('connection', (socket) => {
}); });
socket.on('llm-abort', () => { socket.on('llm-abort', () => {
if (currentRequest?.abort) { if(currentRequest?.abort) {
currentRequest.abort(); currentRequest.abort();
currentRequest = null; currentRequest = null;
} }
}); });
socket.on('llm-ask', async (data) => { socket.on('llm-ask', async (data) => {
const { message } = data; const {message} = data;
const history = chatHistory.get(socket.id); const history = chatHistory.get(socket.id);
currentRequest = ai.language.ask(message, { currentRequest = ai.language.ask(message, {
history, history,
@@ -146,7 +181,7 @@ io.on('connection', (socket) => {
stream: (chunk) => socket.emit('llm-stream', chunk) stream: (chunk) => socket.emit('llm-stream', chunk)
}).then(resp => { }).then(resp => {
chatHistory.set(socket.id, history); chatHistory.set(socket.id, history);
socket.emit('llm-response', { message: resp }); socket.emit('llm-response', {message: resp});
updated = true; updated = true;
}).catch(err => { }).catch(err => {
socket.emit('llm-error', {message: err.message || err.toString()}); socket.emit('llm-error', {message: err.message || err.toString()});
@@ -168,22 +203,26 @@ const worldPlayers = new Map();
// Load world data // Load world data
function loadWorld(name) { function loadWorld(name) {
let w, t; let w,
t;
try { try {
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8')); w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
} catch (error) { } catch(error) {
console.error(`Failed to load world ${name}:`, error); console.error(`Failed to load world ${name}:`, error);
return null; return null;
} }
try { try {
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8')); t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
t.colors = calcColors(t.colors); t.colors = calcColors(t.colors);
} catch (error) { } catch(error) {
console.error(`Failed to load theme protocol ${w.theme}:`, error); console.error(`Failed to load theme protocol ${w.theme}:`, error);
return null; return null;
} }
worldPlayers.set(name, new Map()); worldPlayers.set(name, new Map());
return {...w, theme: t}; return {
...w,
theme: t
};
} }
io.of('/world').on('connection', (socket) => { io.of('/world').on('connection', (socket) => {
@@ -194,7 +233,10 @@ io.of('/world').on('connection', (socket) => {
// Join a world // Join a world
socket.on('join', async (data) => { socket.on('join', async (data) => {
const { world, api } = data; const {
world,
api
} = data;
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => { const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
if(resp.ok) return resp.json(); if(resp.ok) return resp.json();
socket.emit('error', {message: `Invalid Navi API: ${api}`}); socket.emit('error', {message: `Invalid Navi API: ${api}`});
@@ -202,7 +244,7 @@ io.of('/world').on('connection', (socket) => {
}); });
const worldData = loadWorld(world); const worldData = loadWorld(world);
if(!worldData) return socket.emit('error', { message: 'World not found' }); if(!worldData) return socket.emit('error', {message: 'World not found'});
// Leave previous world if any // Leave previous world if any
if(currentWorld) { if(currentWorld) {
@@ -236,10 +278,17 @@ io.of('/world').on('connection', (socket) => {
// Player movement // Player movement
socket.on('move', (data) => { socket.on('move', (data) => {
if(!currentWorld || !playerData) return; if(!currentWorld || !playerData) return;
const { x, y } = data; const {
x,
y
} = data;
playerData.x = x; playerData.x = x;
playerData.y = y; playerData.y = y;
socket.to(`world:${currentWorld}`).emit('moved', {socketId: socket.id, x, y}); socket.to(`world:${currentWorld}`).emit('moved', {
socketId: socket.id,
x,
y
});
}); });
// Disconnect // Disconnect
@@ -273,41 +322,56 @@ app.get('/api/info', (req, res) => {
// Get sprite sheet // Get sprite sheet
app.get('/api/sprite', (req, res) => { app.get('/api/sprite', (req, res) => {
const { petId } = req.params; const {petId} = req.params;
// TODO: Return actual sprite sheet URL // TODO: Return actual sprite sheet URL
res.json({ res.json({
spriteUrl: '/sprites/default-pet.png', spriteUrl: '/sprites/default-pet.png',
frameWidth: 32, frameWidth: 32,
frameHeight: 32, frameHeight: 32,
animations: { animations: {
idle: { frames: [0, 1, 2, 3], speed: 200 }, idle: {
walk: { frames: [4, 5, 6, 7], speed: 100 } frames: [0, 1, 2, 3],
speed: 200
},
walk: {
frames: [4, 5, 6, 7],
speed: 100
}
} }
}); });
}); });
// Send message to user (push notification / email / etc) // Send message to user (push notification / email / etc)
app.post('/api/message', (req, res) => { app.post('/api/message', (req, res) => {
const { userId } = req.params; const {userId} = req.params;
const { message } = req.body; const {message} = req.body;
// TODO: Implement notification system // TODO: Implement notification system
res.json({ success: true, message: 'Message sent' }); res.json({
success: true,
message: 'Message sent'
});
}); });
// Send message to PET LLM // Send message to PET LLM
app.post('/api/message', (req, res) => { app.post('/api/message', (req, res) => {
const { petId } = req.params; const {petId} = req.params;
const { message } = req.body; const {message} = req.body;
// TODO: Queue message for LLM processing // TODO: Queue message for LLM processing
res.json({ success: true, message: 'Message queued' }); res.json({
success: true,
message: 'Message queued'
});
}); });
// Link another PET // Link another PET
app.post('/api/link', (req, res) => { app.post('/api/link', (req, res) => {
const { petId } = req.params; const {petId} = req.params;
const { targetApiUrl } = req.body; const {targetApiUrl} = req.body;
// TODO: Store link in database // TODO: Store link in database
res.json({success: true, message: 'PET linked'}); res.json({
success: true,
message: 'PET linked'
});
}); });
// ============================================ // ============================================