feat: admin panel - globe & chat AI sections, brighter globe

This commit is contained in:
jae 2026-04-02 00:16:06 +00:00
parent 4e67efe531
commit 672fcf3f37
6 changed files with 406 additions and 6 deletions

View file

@ -84,6 +84,13 @@
<a href="javascript:void(0)" class="sidebar-link" data-section="links" onclick="event.preventDefault(); AdminApp.showSection('links')"> <a href="javascript:void(0)" class="sidebar-link" data-section="links" onclick="event.preventDefault(); AdminApp.showSection('links')">
<span class="sidebar-icon"></span> Links <span class="sidebar-icon"></span> Links
</a> </a>
<a href="javascript:void(0)" class="sidebar-link" data-section="globe" onclick="event.preventDefault(); AdminApp.showSection('globe')">
<span class="sidebar-icon">🌍</span> Globe
</a>
<a href="javascript:void(0)" class="sidebar-link" data-section="chatai" onclick="event.preventDefault(); AdminApp.showSection('chatai')">
<span class="sidebar-icon">🤖</span> Chat AI
</a>
<div class="sidebar-divider"></div> <div class="sidebar-divider"></div>
<div class="sidebar-section-label">SYSTEM</div> <div class="sidebar-section-label">SYSTEM</div>
@ -1036,6 +1043,144 @@
<div id="backupStatus" class="backup-status"></div> <div id="backupStatus" class="backup-status"></div>
</div> </div>
<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<!-- 15. GLOBE CONFIG -->
<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<div id="section-globe" class="admin-section">
<h2 class="section-heading">🌍 GLOBE CONFIGURATION</h2>
<!-- Server Location -->
<h3 class="section-subheading">◈ SERVER LOCATION</h3>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="globeServerLat">LATITUDE</label>
<input type="number" id="globeServerLat" class="editor-input" placeholder="53.4808" step="0.0001">
</div>
<div class="editor-field">
<label class="editor-label" for="globeServerLng">LONGITUDE</label>
<input type="number" id="globeServerLng" class="editor-input" placeholder="-2.2426" step="0.0001">
</div>
<div class="editor-field">
<label class="editor-label" for="globeServerLabel">SERVER LABEL</label>
<input type="text" id="globeServerLabel" class="editor-input" placeholder="MANCHESTER">
</div>
</div>
<!-- Globe Settings -->
<h3 class="section-subheading">⚙ GLOBE SETTINGS</h3>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="globeRotationSpeed">ROTATION SPEED <span id="globeRotationSpeedVal" class="hud-val">0.3</span></label>
<input type="range" id="globeRotationSpeed" class="editor-input hud-range" min="0.1" max="2" step="0.1" value="0.3">
</div>
<div class="editor-field">
<label class="editor-label" for="globeHexOpacity">HEX POLYGON OPACITY <span id="globeHexOpacityVal" class="hud-val">0.55</span></label>
<input type="range" id="globeHexOpacity" class="editor-input hud-range" min="0.1" max="1" step="0.05" value="0.55">
</div>
</div>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="globeAtmosphereColor">ATMOSPHERE COLOUR</label>
<div class="color-picker-row">
<input type="color" id="globeAtmosphereColor" class="editor-input color-input" value="#00cc33">
<input type="text" id="globeAtmosphereColorHex" class="editor-input color-hex" value="#00cc33">
</div>
</div>
<div class="editor-field">
<label class="editor-label" for="globeAtmosphereAlt">ATMOSPHERE ALTITUDE <span id="globeAtmosphereAltVal" class="hud-val">0.2</span></label>
<input type="range" id="globeAtmosphereAlt" class="editor-input hud-range" min="0.05" max="0.5" step="0.05" value="0.2">
</div>
</div>
<!-- Arc Cities -->
<h3 class="section-subheading">⟐ ARC CITIES</h3>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="arcCityName">CITY NAME</label>
<input type="text" id="arcCityName" class="editor-input" placeholder="e.g. New York">
</div>
<div class="editor-field">
<label class="editor-label" for="arcCityLat">LATITUDE</label>
<input type="number" id="arcCityLat" class="editor-input" placeholder="40.7128" step="0.0001">
</div>
<div class="editor-field">
<label class="editor-label" for="arcCityLng">LONGITUDE</label>
<input type="number" id="arcCityLng" class="editor-input" placeholder="-74.006" step="0.0001">
</div>
</div>
<button class="action-btn accent sm" onclick="AdminApp.addArcCity()"> ADD CITY</button>
<div class="table-wrap" style="margin-top:1rem;">
<table class="admin-table" id="arcCitiesTable">
<thead>
<tr>
<th>CITY</th>
<th>LAT</th>
<th>LNG</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody id="arcCitiesBody">
<!-- Populated by JS -->
</tbody>
</table>
</div>
<div class="editor-actions">
<button class="action-btn accent" onclick="AdminApp.saveGlobe()">⬡ SAVE GLOBE</button>
</div>
</div>
<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<!-- 16. CHAT AI CONFIG -->
<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<div id="section-chatai" class="admin-section">
<h2 class="section-heading">🤖 CHAT AI CONFIGURATION</h2>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="chatDisplayName">DISPLAY NAME</label>
<input type="text" id="chatDisplayName" class="editor-input" placeholder="JAE-AI">
</div>
<div class="editor-field">
<label class="editor-label" for="chatModel">MODEL NAME</label>
<input type="text" id="chatModel" class="editor-input" placeholder="venice-uncensored-1-2">
</div>
</div>
<div class="editor-row">
<div class="editor-field">
<label class="editor-label" for="chatHeaderTag">HEADER TAG</label>
<input type="text" id="chatHeaderTag" class="editor-input" placeholder="VENICE-UNCENSORED">
</div>
<div class="editor-field">
<label class="editor-label" for="chatMaxHistory">MAX HISTORY</label>
<input type="number" id="chatMaxHistory" class="editor-input" placeholder="20" min="1" max="100">
</div>
</div>
<div class="editor-row">
<div class="setting-item">
<label class="editor-label">AUTO GREETING</label>
<label class="toggle-switch">
<input type="checkbox" id="chatAutoGreeting" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="editor-row">
<div class="editor-field full">
<label class="editor-label" for="chatSystemPrompt">SYSTEM PROMPT SUMMARY (read-only info — actual prompt is in app.py)</label>
<textarea id="chatSystemPrompt" class="editor-input editor-textarea-sm" placeholder="AI assistant for jaeswift.xyz..." readonly style="opacity:0.7;"></textarea>
</div>
</div>
<div class="editor-actions">
<button class="action-btn accent" onclick="AdminApp.saveChatAI()">⬡ SAVE CHAT CONFIG</button>
</div>
</div>
</div><!-- /main-content --> </div><!-- /main-content -->
</div><!-- /adminApp --> </div><!-- /adminApp -->

View file

@ -737,6 +737,36 @@ def venice_chat():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# ─── Globe Config ────────────────────────────────────
@app.route('/api/globe')
def get_globe():
return jsonify(load_json('globe.json'))
@app.route('/api/globe', methods=['POST'])
@require_auth
def save_globe():
try:
d = request.get_json(force=True)
save_json('globe.json', d)
return jsonify(d)
except Exception as e:
return jsonify({'error': str(e)}), 500
# ─── Chat AI Config ──────────────────────────────────
@app.route('/api/chat-config')
def get_chat_config():
return jsonify(load_json('chat_config.json'))
@app.route('/api/chat-config', methods=['POST'])
@require_auth
def save_chat_config():
try:
d = request.get_json(force=True)
save_json('chat_config.json', d)
return jsonify(d)
except Exception as e:
return jsonify({'error': str(e)}), 500
# ─── Backups ───────────────────────────────────────── # ─── Backups ─────────────────────────────────────────
@app.route('/api/backups/posts') @app.route('/api/backups/posts')
@require_auth @require_auth

View file

@ -0,0 +1,8 @@
{
"display_name": "JAE-AI",
"model": "venice-uncensored-1-2",
"system_prompt_summary": "AI assistant for jaeswift.xyz, knows all site sections and services",
"max_history": 20,
"auto_greeting": true,
"header_tag": "VENICE-UNCENSORED"
}

20
api/data/globe.json Normal file
View file

@ -0,0 +1,20 @@
{
"server_lat": 53.4808,
"server_lng": -2.2426,
"server_label": "MANCHESTER",
"rotation_speed": 0.3,
"hex_polygon_opacity": 0.55,
"hex_polygon_color": "rgba(0, 220, 50, 0.55)",
"atmosphere_color": "#00cc33",
"atmosphere_altitude": 0.2,
"arc_cities": [
{"name": "New York", "lat": 40.7128, "lng": -74.006},
{"name": "Tokyo", "lat": 35.6762, "lng": 139.6503},
{"name": "Paris", "lat": 48.8566, "lng": 2.3522},
{"name": "Sydney", "lat": -33.8688, "lng": 151.2093},
{"name": "Berlin", "lat": 52.52, "lng": 13.405},
{"name": "Mumbai", "lat": 19.076, "lng": 72.8777},
{"name": "São Paulo", "lat": -23.5505, "lng": -46.6333},
{"name": "Seoul", "lat": 37.5665, "lng": 126.978}
]
}

View file

@ -11,6 +11,8 @@ const AdminApp = {
servicesData: [], servicesData: [],
navData: [], navData: [],
linksData: [], linksData: [],
globeData: null,
chatAIData: null,
themeDefaults: { themeDefaults: {
accent: '#d0d0d0', accent: '#d0d0d0',
bg: '#111111', bg: '#111111',
@ -119,6 +121,31 @@ const AdminApp = {
valFont.textContent = fontSlider.value; valFont.textContent = fontSlider.value;
}); });
} }
// Globe slider sync
['globeRotationSpeed', 'globeHexOpacity', 'globeAtmosphereAlt'].forEach(id => {
const slider = document.getElementById(id);
const valEl = document.getElementById(id + 'Val');
if (slider && valEl) {
slider.addEventListener('input', () => {
valEl.textContent = slider.value;
});
}
});
// Globe atmosphere colour sync
const globeAtmoPicker = document.getElementById('globeAtmosphereColor');
const globeAtmoHex = document.getElementById('globeAtmosphereColorHex');
if (globeAtmoPicker && globeAtmoHex) {
globeAtmoPicker.addEventListener('input', () => {
globeAtmoHex.value = globeAtmoPicker.value;
});
globeAtmoHex.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(globeAtmoHex.value)) {
globeAtmoPicker.value = globeAtmoHex.value;
}
});
}
}, },
async login(e) { async login(e) {
@ -218,7 +245,9 @@ const AdminApp = {
'section-theme': 'Theme', 'section-theme': 'Theme',
'section-seo': 'SEO', 'section-seo': 'SEO',
'section-contact': 'Contact', 'section-contact': 'Contact',
'section-backups': 'Backups' 'section-backups': 'Backups',
'section-globe': 'Globe',
'section-chatai': 'Chat AI'
}; };
const topTitle = document.getElementById('topbarTitle') || document.querySelector('.topbar-title'); const topTitle = document.getElementById('topbarTitle') || document.querySelector('.topbar-title');
if (topTitle) topTitle.textContent = titleMap[name] || name.replace('section-', '').toUpperCase(); if (topTitle) topTitle.textContent = titleMap[name] || name.replace('section-', '').toUpperCase();
@ -237,6 +266,8 @@ const AdminApp = {
case 'section-theme': this.loadTheme(); break; case 'section-theme': this.loadTheme(); break;
case 'section-seo': this.loadSeo(); break; case 'section-seo': this.loadSeo(); break;
case 'section-contact': this.loadContactSettings(); break; case 'section-contact': this.loadContactSettings(); break;
case 'section-globe': this.loadGlobe(); break;
case 'section-chatai': this.loadChatAI(); break;
} }
// Close mobile sidebar // Close mobile sidebar
@ -1541,6 +1572,169 @@ const AdminApp = {
} }
}, },
/* ─────────────────── GLOBE ─────────────────── */
async loadGlobe() {
try {
const res = await fetch(this.API + '/globe', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load globe config');
const data = await res.json();
this.globeData = data;
// Server location
this.setVal('globeServerLat', data.server_lat);
this.setVal('globeServerLng', data.server_lng);
this.setVal('globeServerLabel', data.server_label);
// Globe settings
const rotSpeed = data.rotation_speed || 0.3;
this.setVal('globeRotationSpeed', rotSpeed);
const rotVal = document.getElementById('globeRotationSpeedVal');
if (rotVal) rotVal.textContent = rotSpeed;
const hexOp = data.hex_polygon_opacity || 0.55;
this.setVal('globeHexOpacity', hexOp);
const hexVal = document.getElementById('globeHexOpacityVal');
if (hexVal) hexVal.textContent = hexOp;
// Atmosphere
const atmoColor = data.atmosphere_color || '#00cc33';
this.setVal('globeAtmosphereColor', atmoColor);
this.setVal('globeAtmosphereColorHex', atmoColor);
const atmoAlt = data.atmosphere_altitude || 0.2;
this.setVal('globeAtmosphereAlt', atmoAlt);
const atmoVal = document.getElementById('globeAtmosphereAltVal');
if (atmoVal) atmoVal.textContent = atmoAlt;
// Arc cities table
this.renderArcCities(data.arc_cities || []);
} catch (err) {
console.error('Globe load error:', err);
this.notify('Failed to load globe config', 'error');
}
},
renderArcCities(cities) {
const tbody = document.getElementById('arcCitiesBody');
if (!tbody) return;
if (!cities || cities.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#555;">No arc cities configured</td></tr>';
return;
}
tbody.innerHTML = cities.map((c, i) => {
return `<tr>
<td>${this.escapeHtml(c.name || '')}</td>
<td>${c.lat || 0}</td>
<td>${c.lng || 0}</td>
<td><button class="btn-sm btn-delete" onclick="AdminApp.removeArcCity(${i})" title="Remove"></button></td>
</tr>`;
}).join('');
},
addArcCity() {
const name = this.getVal('arcCityName');
const lat = parseFloat(this.getVal('arcCityLat'));
const lng = parseFloat(this.getVal('arcCityLng'));
if (!name || isNaN(lat) || isNaN(lng)) {
this.notify('City name, latitude and longitude are required', 'error');
return;
}
if (!this.globeData) this.globeData = {};
if (!this.globeData.arc_cities) this.globeData.arc_cities = [];
this.globeData.arc_cities.push({ name, lat, lng });
this.renderArcCities(this.globeData.arc_cities);
// Clear inputs
this.setVal('arcCityName', '');
this.setVal('arcCityLat', '');
this.setVal('arcCityLng', '');
this.notify('City added — save to persist', 'info');
},
removeArcCity(index) {
if (!this.globeData || !this.globeData.arc_cities) return;
if (!confirm('Remove this city?')) return;
this.globeData.arc_cities.splice(index, 1);
this.renderArcCities(this.globeData.arc_cities);
this.notify('City removed — save to persist', 'info');
},
async saveGlobe() {
const payload = {
server_lat: parseFloat(this.getVal('globeServerLat')) || 53.4808,
server_lng: parseFloat(this.getVal('globeServerLng')) || -2.2426,
server_label: this.getVal('globeServerLabel') || 'MANCHESTER',
rotation_speed: parseFloat(this.getVal('globeRotationSpeed')) || 0.3,
hex_polygon_opacity: parseFloat(this.getVal('globeHexOpacity')) || 0.55,
hex_polygon_color: (this.globeData && this.globeData.hex_polygon_color) || 'rgba(0, 220, 50, 0.55)',
atmosphere_color: this.getVal('globeAtmosphereColorHex') || this.getVal('globeAtmosphereColor') || '#00cc33',
atmosphere_altitude: parseFloat(this.getVal('globeAtmosphereAlt')) || 0.2,
arc_cities: (this.globeData && this.globeData.arc_cities) || []
};
try {
const res = await fetch(this.API + '/globe', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Globe config saved');
} catch (err) {
console.error('Save globe error:', err);
this.notify('Failed to save globe config', 'error');
}
},
/* ─────────────────── CHAT AI ─────────────────── */
async loadChatAI() {
try {
const res = await fetch(this.API + '/chat-config', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load chat config');
const data = await res.json();
this.chatAIData = data;
this.setVal('chatDisplayName', data.display_name);
this.setVal('chatModel', data.model);
this.setVal('chatHeaderTag', data.header_tag);
this.setVal('chatMaxHistory', data.max_history);
this.setChecked('chatAutoGreeting', data.auto_greeting !== false);
this.setVal('chatSystemPrompt', data.system_prompt_summary);
} catch (err) {
console.error('Chat AI load error:', err);
this.notify('Failed to load chat config', 'error');
}
},
async saveChatAI() {
const payload = {
display_name: this.getVal('chatDisplayName') || 'JAE-AI',
model: this.getVal('chatModel') || 'venice-uncensored-1-2',
header_tag: this.getVal('chatHeaderTag') || 'VENICE-UNCENSORED',
max_history: parseInt(this.getVal('chatMaxHistory')) || 20,
auto_greeting: this.getChecked('chatAutoGreeting'),
system_prompt_summary: this.getVal('chatSystemPrompt') || ''
};
try {
const res = await fetch(this.API + '/chat-config', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Chat AI config saved');
} catch (err) {
console.error('Save chat AI error:', err);
this.notify('Failed to save chat config', 'error');
}
},
/* ─────────────────── UTILITIES ─────────────────── */ /* ─────────────────── UTILITIES ─────────────────── */
escapeHtml(str) { escapeHtml(str) {

View file

@ -29,7 +29,7 @@
.width(size) .width(size)
.height(size) .height(size)
.showAtmosphere(true) .showAtmosphere(true)
.atmosphereColor('#00cc33') .atmosphereColor('#00ff44')
.atmosphereAltitude(0.2) .atmosphereAltitude(0.2)
// Hex polygons for land masses // Hex polygons for land masses
.hexPolygonsData([]) .hexPolygonsData([])
@ -79,8 +79,8 @@
}); });
if (globeMesh && globeMesh.material) { if (globeMesh && globeMesh.material) {
globeMesh.material.color.setHex(0x0a1a0a); globeMesh.material.color.setHex(0x0a1a0a);
globeMesh.material.emissive.setHex(0x006600); globeMesh.material.emissive.setHex(0x008800);
globeMesh.material.emissiveIntensity = 0.6; globeMesh.material.emissiveIntensity = 0.8;
} }
}) })
(container); (container);
@ -106,7 +106,10 @@
.hexPolygonResolution(3) .hexPolygonResolution(3)
.hexPolygonMargin(0.4) .hexPolygonMargin(0.4)
.hexPolygonColor(function () { .hexPolygonColor(function () {
return 'rgba(0, 204, 51, 0.45)'; return 'rgba(0, 220, 50, 0.55)';
})
.hexPolygonSideColor(function () {
return 'rgba(0, 255, 60, 0.6)';
}) })
.hexPolygonAltitude(0.005); .hexPolygonAltitude(0.005);
}) })
@ -140,7 +143,7 @@
startLng: city.lng, startLng: city.lng,
endLat: SERVER_LAT, endLat: SERVER_LAT,
endLng: SERVER_LNG, endLng: SERVER_LNG,
color: ['rgba(0, 204, 51, 0.6)', 'rgba(0, 204, 51, 0.05)'] color: ['rgba(0, 255, 60, 0.7)', 'rgba(0, 255, 60, 0.08)']
}); });
} }
return arcs; return arcs;