This commit was merged in pull request #1337.
This commit is contained in:
@@ -507,7 +507,7 @@
|
|||||||
if (!dot || !label) return;
|
if (!dot || !label) return;
|
||||||
if (!wsConnected) {
|
if (!wsConnected) {
|
||||||
dot.className = 'mc-conn-dot red';
|
dot.className = 'mc-conn-dot red';
|
||||||
label.textContent = 'OFFLINE';
|
label.textContent = 'Reconnecting...';
|
||||||
} else if (ollamaOk === false) {
|
} else if (ollamaOk === false) {
|
||||||
dot.className = 'mc-conn-dot amber';
|
dot.className = 'mc-conn-dot amber';
|
||||||
label.textContent = 'NO LLM';
|
label.textContent = 'NO LLM';
|
||||||
@@ -543,7 +543,12 @@
|
|||||||
var ws;
|
var ws;
|
||||||
try {
|
try {
|
||||||
ws = new WebSocket(protocol + '//' + window.location.host + '/swarm/live');
|
ws = new WebSocket(protocol + '//' + window.location.host + '/swarm/live');
|
||||||
} catch(e) { return; }
|
} catch(e) {
|
||||||
|
// WebSocket constructor failed (e.g. invalid environment) — retry
|
||||||
|
setTimeout(connectStatusWs, reconnectDelay);
|
||||||
|
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ws.onopen = function() {
|
ws.onopen = function() {
|
||||||
wsConnected = true;
|
wsConnected = true;
|
||||||
|
|||||||
@@ -103,6 +103,28 @@
|
|||||||
<div id="submit-job-backdrop" class="submit-job-backdrop"></div>
|
<div id="submit-job-backdrop" class="submit-job-backdrop"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- GPU context loss overlay -->
|
||||||
|
<div id="gpu-error-overlay" class="hidden">
|
||||||
|
<div class="gpu-error-icon">🔮</div>
|
||||||
|
<div class="gpu-error-title">Timmy is taking a quick break</div>
|
||||||
|
<div class="gpu-error-msg">
|
||||||
|
The 3D workshop lost its connection to the GPU.<br>
|
||||||
|
Attempting to restore the scene…
|
||||||
|
</div>
|
||||||
|
<div class="gpu-error-retry" id="gpu-retry-msg">Reconnecting automatically…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WebGL / mobile fallback -->
|
||||||
|
<div id="webgl-fallback" class="hidden">
|
||||||
|
<div class="fallback-title">Timmy</div>
|
||||||
|
<div class="fallback-subtitle">The Workshop</div>
|
||||||
|
<div class="fallback-status">
|
||||||
|
<div class="mood-line" id="fallback-mood">focused</div>
|
||||||
|
<p id="fallback-detail">Pondering the arcane arts of code…</p>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="fallback-link">Open Mission Control</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- About Panel -->
|
<!-- About Panel -->
|
||||||
<div id="about-panel" class="about-panel">
|
<div id="about-panel" class="about-panel">
|
||||||
<div class="about-panel-content">
|
<div class="about-panel-content">
|
||||||
@@ -157,6 +179,38 @@
|
|||||||
import { StateReader } from "./state.js";
|
import { StateReader } from "./state.js";
|
||||||
import { messageQueue } from "./queue.js";
|
import { messageQueue } from "./queue.js";
|
||||||
|
|
||||||
|
// --- Mobile detection: redirect to text interface on small touch devices ---
|
||||||
|
const isMobile = window.matchMedia("(pointer: coarse)").matches
|
||||||
|
&& window.innerWidth < 768;
|
||||||
|
if (isMobile) {
|
||||||
|
const fallback = document.getElementById("webgl-fallback");
|
||||||
|
if (fallback) fallback.classList.remove("hidden");
|
||||||
|
// Don't initialise the 3D scene on mobile
|
||||||
|
throw new Error("Mobile device — 3D scene skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WebGL support detection ---
|
||||||
|
function _hasWebGL() {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
return !!(
|
||||||
|
canvas.getContext("webgl2") ||
|
||||||
|
canvas.getContext("webgl") ||
|
||||||
|
canvas.getContext("experimental-webgl")
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_hasWebGL()) {
|
||||||
|
const fallback = document.getElementById("webgl-fallback");
|
||||||
|
const detail = document.getElementById("fallback-detail");
|
||||||
|
if (fallback) fallback.classList.remove("hidden");
|
||||||
|
if (detail) detail.textContent =
|
||||||
|
"Your device doesn\u2019t support WebGL. Use a modern browser to see the 3D workshop.";
|
||||||
|
throw new Error("WebGL not supported");
|
||||||
|
}
|
||||||
|
|
||||||
// --- Renderer ---
|
// --- Renderer ---
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
@@ -167,6 +221,26 @@
|
|||||||
renderer.toneMappingExposure = 0.8;
|
renderer.toneMappingExposure = 0.8;
|
||||||
document.body.prepend(renderer.domElement);
|
document.body.prepend(renderer.domElement);
|
||||||
|
|
||||||
|
// --- WebGL context loss / restore ---
|
||||||
|
const _gpuOverlay = document.getElementById("gpu-error-overlay");
|
||||||
|
const _gpuRetryMsg = document.getElementById("gpu-retry-msg");
|
||||||
|
let _animating = true;
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("webglcontextlost", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
_animating = false;
|
||||||
|
if (_gpuOverlay) _gpuOverlay.classList.remove("hidden");
|
||||||
|
if (_gpuRetryMsg) _gpuRetryMsg.textContent = "Reconnecting automatically\u2026";
|
||||||
|
console.warn("[Workshop] WebGL context lost — waiting for restore");
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("webglcontextrestored", () => {
|
||||||
|
_animating = true;
|
||||||
|
if (_gpuOverlay) _gpuOverlay.classList.add("hidden");
|
||||||
|
console.info("[Workshop] WebGL context restored — resuming");
|
||||||
|
animate();
|
||||||
|
}, false);
|
||||||
|
|
||||||
// --- Scene ---
|
// --- Scene ---
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0x0a0a14);
|
scene.background = new THREE.Color(0x0a0a14);
|
||||||
@@ -195,6 +269,13 @@
|
|||||||
if (moodEl) {
|
if (moodEl) {
|
||||||
moodEl.textContent = state.timmyState.mood;
|
moodEl.textContent = state.timmyState.mood;
|
||||||
}
|
}
|
||||||
|
// Keep fallback view in sync when it's visible
|
||||||
|
const fallbackMood = document.getElementById("fallback-mood");
|
||||||
|
const fallbackDetail = document.getElementById("fallback-detail");
|
||||||
|
if (fallbackMood) fallbackMood.textContent = state.timmyState.mood;
|
||||||
|
if (fallbackDetail && state.timmyState.activity) {
|
||||||
|
fallbackDetail.textContent = state.timmyState.activity + "\u2026";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replay queued jobs whenever the server comes back online.
|
// Replay queued jobs whenever the server comes back online.
|
||||||
@@ -537,6 +618,7 @@
|
|||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
|
if (!_animating) return;
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
const dt = clock.getDelta();
|
const dt = clock.getDelta();
|
||||||
|
|
||||||
|
|||||||
@@ -715,3 +715,116 @@ canvas {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* GPU context loss overlay */
|
||||||
|
#gpu-error-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(10, 10, 20, 0.95);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gpu-error-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpu-error-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpu-error-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #daa520;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpu-error-msg {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #aaa;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpu-error-retry {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebGL / mobile fallback — text-only mode */
|
||||||
|
#webgl-fallback {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #0a0a14;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webgl-fallback.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-title {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #daa520;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-status {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #aaa;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-status .mood-line {
|
||||||
|
color: #daa520;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: 1px solid rgba(218, 165, 32, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #daa520;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-link:hover {
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
border-color: rgba(218, 165, 32, 0.7);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user