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

View File

@@ -6,21 +6,61 @@ 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
// ============================================
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const __dirname = join(dirname(__filename), '..');
const storage = join(__dirname, 'storage');
const navi = join(storage, 'navi');
const protocols = join(storage, 'protocols');
const worlds = join(storage, 'worlds');
const logoFile = join(navi, 'logo.png');
const settingsFile = join(navi, 'settings.json');
const memoriesFile = join(navi, 'memories.json');
let updated = false;
const app = express();
const httpServer = createServer(app);
function calcColors(theme) {
return {
...theme,
backgroundContrast: contrast(theme.background),
backgroundDark: shadeColor(theme.background, -.1),
backgroundLight: shadeColor(theme.background, .1),
primaryContrast: contrast(theme.primary),
primaryDark: shadeColor(theme.primary, -.1),
primaryLight: shadeColor(theme.primary, .1),
accentContrast: contrast(theme.accent),
accentDark: shadeColor(theme.accent, -.1),
accentLight: shadeColor(theme.accent, .1),
mutedContrast: contrast(theme.muted),
mutedDark: shadeColor(theme.muted, -.1),
mutedLight: shadeColor(theme.muted, .1),
}
}
let memories = [], settings = {};
const settingsFile = join(__dirname, '../navi', 'settings.json');
const memoriesFile = join(__dirname, '../navi', 'memories.json');
const logoFile = join(__dirname, '../navi', 'logo.png');
let memories = [], settings = {
name: 'Navi',
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
instructions: '',
theme: {
background: '#fff',
border: '#000',
text: '#252525',
primary: '#9f32ef',
accent: '#6f16c3',
muted: '#a8a8a8',
}
};
function load() {
try {
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
settings = {
...settings,
...JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))
};
} catch { }
try {
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
@@ -28,6 +68,9 @@ function load() {
}
function save() {
if(!updated) return;
updated = false;
const dir = dirname(settingsFile);
if (!fs.existsSync(dir)) {
fs.mkdir(dir, { recursive: true }, (err) => {
@@ -38,11 +81,6 @@ function save() {
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
}
function shutdown() {
save();
process.exit(0);
}
load();
const ai = new Ai({
llm: {
@@ -56,13 +94,19 @@ const ai = new Ai({
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}},
fn: (args) => {
settings.personality = args.instructions;
save();
updated = true;
return 'done!';
}
}],
},
});
// ============================================
// Setup
// ============================================
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {origin: "*", methods: ["GET", "POST"]}
});
@@ -72,133 +116,145 @@ app.use(express.json());
app.use(express.static('public'));
// ============================================
// WORLD MANAGEMENT
// Primary Socket
// ============================================
const worldPlayers = new Map();
const chatHistory = new Map();
// Load world data
function loadWorld(worldId) {
try {
const worldPath = join(__dirname, '../worlds', worldId || '', 'world.json');
const world = JSON.parse(fs.readFileSync(worldPath, 'utf-8'));
const themePath = join(__dirname, '../worlds', worldId || '', world.theme);
const theme = JSON.parse(fs.readFileSync(themePath, 'utf-8'));
worldPlayers.set(worldId, new Map());
return {world, theme};
} catch (error) {
console.error(`Failed to load world ${worldId}:`, error);
return null;
}
}
// ============================================
// SOCKET.IO - WORLD CHANNELS
// ============================================
io.on('connection', (socket) => {
console.debug('🔌 Client connected:', socket.id);
console.debug('👤 User connected:', socket.id);
let currentWorld = null;
let playerData = null;
// Join a world
socket.on('join-world', (data) => {
const { worldId, playerInfo } = data;
const worldData = loadWorld(worldId);
if(!worldData) return socket.emit('error', { message: 'World not found' });
// Leave previous world if any
if(currentWorld) {
socket.leave(`world:${currentWorld}`);
const players = worldPlayers.get(currentWorld);
if(players) {
players.delete(socket.id);
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
}
}
// Join new world
currentWorld = worldId;
playerData = {
socketId: socket.id,
name: playerInfo.name,
apiUrl: playerInfo.apiUrl,
x: worldData.world.pet.startX,
y: worldData.world.pet.startY
};
socket.join(`world:${worldId}`);
const players = worldPlayers.get(worldId);
players.set(socket.id, playerData);
socket.emit('world-data', worldData);
const currentPlayers = Array.from(players.values());
socket.emit('current-players', currentPlayers);
socket.to(`world:${worldId}`).emit('player-joined', playerData);
});
// Player movement
socket.on('player-move', (data) => {
if(!currentWorld || !playerData) return;
const { x, y } = data;
playerData.x = x;
playerData.y = y;
socket.to(`world:${currentWorld}`).emit('player-moved', {socketId: socket.id, x, y});
});
// Disconnect
socket.on('disconnect', () => {
console.debug('🔌 Client disconnected:', socket.id);
if(currentWorld) {
const players = worldPlayers.get(currentWorld);
if(players) {
players.delete(socket.id);
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
}
}
});
});
// ============================================
// LLM CHANNEL
// ============================================
const petNamespace = io.of('/llm');
petNamespace.on('connection', (socket) => {
chatHistory.set(socket.id, []);
let currentRequest = null;
socket.on('clear', async () => {
socket.on('llm-clear', async () => {
chatHistory.set(socket.id, []);
});
socket.on('abort', () => {
socket.on('llm-abort', () => {
if (currentRequest?.abort) {
currentRequest.abort();
currentRequest = null;
}
});
socket.on('message', async (data) => {
const { message, apiUrl } = data;
socket.on('llm-ask', async (data) => {
const { message } = data;
const history = chatHistory.get(socket.id);
currentRequest = ai.language.ask(message, {
history,
memory: memories,
stream: (chunk) => socket.emit('stream', chunk)
stream: (chunk) => socket.emit('llm-stream', chunk)
}).then(resp => {
chatHistory.set(socket.id, history);
socket.emit('response', { message: resp });
socket.emit('llm-response', { message: resp });
updated = true;
}).catch(err => {
socket.emit('error', {message: err.message || err.toString()});
socket.emit('llm-error', {message: err.message || err.toString()});
}).finally(() => {
currentRequest = null;
});
});
socket.on('disconnect', () => {
console.log('🔌 LLM Client disconnected:', socket.id);
console.log('👤 User disconnected:', socket.id);
chatHistory.delete(socket.id);
});
});
// ============================================
// World Socket
// ============================================
const worldPlayers = new Map();
// Load world data
function loadWorld(name) {
let w, t;
try {
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
} catch (error) {
console.error(`Failed to load world ${name}:`, error);
return null;
}
try {
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
t.colors = calcColors(t.colors);
} catch (error) {
console.error(`Failed to load theme protocol ${w.theme}:`, error);
return null;
}
worldPlayers.set(name, new Map());
return {...w, theme: t};
}
io.of('/world').on('connection', (socket) => {
console.debug('🌍 Navi joined world:', socket.id);
let currentWorld = null;
let playerData = null;
// Join a world
socket.on('join', async (data) => {
const { world, api } = data;
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
if(resp.ok) return resp.json();
socket.emit('error', {message: `Invalid Navi API: ${api}`});
return resp.error;
});
const worldData = loadWorld(world);
if(!worldData) return socket.emit('error', { message: 'World not found' });
// Leave previous world if any
if(currentWorld) {
socket.leave(`world:${currentWorld}`);
const players = worldPlayers.get(currentWorld);
if(players) {
players.delete(socket.id);
socket.to(`world:${currentWorld}`).emit('left', {socketId: socket.id});
}
}
// Join new world
currentWorld = world;
const spawn = worldData.tiles.find(t => t.type === 'spawn');
playerData = {
...info,
socketId: socket.id,
navi: api,
x: spawn.x,
y: spawn.y
};
socket.join(`world:${world}`);
const players = worldPlayers.get(world);
players.set(socket.id, playerData);
socket.emit('data', worldData);
const currentPlayers = Array.from(players.values());
socket.emit('players', currentPlayers);
socket.to(`world:${world}`).emit('joined', playerData);
});
// Player movement
socket.on('move', (data) => {
if(!currentWorld || !playerData) return;
const { x, y } = data;
playerData.x = x;
playerData.y = y;
socket.to(`world:${currentWorld}`).emit('moved', {socketId: socket.id, x, y});
});
// Disconnect
socket.on('disconnect', () => {
console.debug('🌍 Navi disconnected:', socket.id);
if(currentWorld) {
const players = worldPlayers.get(currentWorld);
if(players) {
players.delete(socket.id);
socket.to(`world:${currentWorld}`).emit('left', {socketId: socket.id});
}
}
});
});
// ============================================
// REST API ENDPOINTS
// ============================================
@@ -207,21 +263,12 @@ app.get('/favicon.*', (req, res) => {
res.sendFile(logoFile);
});
// Get PET info
// Get Navi info
app.get('/api/info', (req, res) => {
const { petId } = req.params;
// TODO: Fetch from database
res.json({
id: petId,
name: 'MyCoolPET',
owner: 'player1',
bandwidth: 75,
shards: [],
stats: {
level: 5,
health: 100
}
});
name: settings.name,
theme: calcColors(settings.theme),
});
});
// Get sprite sheet
@@ -268,11 +315,15 @@ app.post('/api/link', (req, res) => {
// ============================================
const PORT = process.env.PORT || 3000;
setInterval(() => save(), 5 * 60_000);
httpServer.listen(PORT, () => {
loadWorld();
console.log('✅ Home world loaded');
console.log(`🚀 Server running on: http://localhost:${PORT}`);
});
function shutdown() {
save();
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);