This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/static/world/index.html

495 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Timmy's Workshop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="overlay">
<div id="status">
<div class="name">Timmy</div>
<div class="mood" id="mood-text">focused</div>
</div>
<div id="connection-dot"></div>
<button id="info-btn" class="info-button" aria-label="About The Matrix" title="About The Matrix">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</button>
<button id="submit-job-btn" class="submit-job-button" aria-label="Submit Job" title="Submit Job">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
<span>Job</span>
</button>
<div id="speech-area">
<div class="bubble" id="speech-bubble"></div>
</div>
</div>
<!-- Submit Job Modal -->
<div id="submit-job-modal" class="submit-job-modal">
<div class="submit-job-content">
<button id="submit-job-close" class="submit-job-close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<h2>Submit Job</h2>
<p class="submit-job-subtitle">Create a task for Timmy and the agent swarm</p>
<form id="submit-job-form" class="submit-job-form">
<div class="form-group">
<label for="job-title">Title <span class="required">*</span></label>
<input type="text" id="job-title" name="title" placeholder="Brief description of the task" maxlength="200">
<div class="char-count" id="title-char-count">0 / 200</div>
<div class="validation-error" id="title-error"></div>
</div>
<div class="form-group">
<label for="job-description">Description</label>
<textarea id="job-description" name="description" placeholder="Detailed instructions, requirements, and context..." rows="6" maxlength="2000"></textarea>
<div class="char-count" id="desc-char-count">0 / 2000</div>
<div class="validation-warning" id="desc-warning"></div>
<div class="validation-error" id="desc-error"></div>
</div>
<div class="form-group">
<label for="job-priority">Priority</label>
<select id="job-priority" name="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="submit-job-actions">
<button type="button" id="cancel-job-btn" class="btn-secondary">Cancel</button>
<button type="submit" id="submit-job-submit" class="btn-primary" disabled>Submit Job</button>
</div>
</form>
<div id="submit-job-success" class="submit-job-success hidden">
<div class="success-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h3>Job Submitted!</h3>
<p>Your task has been added to the queue. Timmy will review it shortly.</p>
<button type="button" id="submit-another-btn" class="btn-primary">Submit Another</button>
</div>
</div>
<div id="submit-job-backdrop" class="submit-job-backdrop"></div>
</div>
<!-- About Panel -->
<div id="about-panel" class="about-panel">
<div class="about-panel-content">
<button id="about-close" class="about-close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<h2>Welcome to The Matrix</h2>
<section>
<h3>🌌 The Matrix</h3>
<p>The Matrix is a 3D visualization of Timmy's AI agent workspace. Enter the workshop to see Timmy at work—pondering the arcane arts of code, managing tasks, and orchestrating autonomous agents in real-time.</p>
</section>
<section>
<h3>🛠️ The Workshop</h3>
<p>The Workshop is where you interact directly with Timmy:</p>
<ul>
<li><strong>Submit Jobs</strong> — Create tasks, delegate work, and track progress</li>
<li><strong>Chat with Agents</strong> — Converse with Timmy and his swarm of specialized agents</li>
<li><strong>Fund Sessions</strong> — Power your work with satoshis via Lightning Network</li>
</ul>
</section>
<section>
<h3>⚡ Lightning & Sats</h3>
<p>The Matrix runs on Bitcoin. Sessions are funded with satoshis (sats) over the Lightning Network—enabling fast, cheap micropayments that keep Timmy energized and working for you. No subscriptions, no limits—pay as you go.</p>
</section>
<div class="about-footer">
<span>Sovereign AI · Soul on Bitcoin</span>
</div>
</div>
<div id="about-backdrop" class="about-backdrop"></div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { buildRoom } from "./scene.js";
import { createWizard } from "./wizard.js";
import { createFamiliar } from "./familiar.js";
import { setupControls } from "./controls.js";
import { StateReader } from "./state.js";
// --- Renderer ---
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.8;
document.body.prepend(renderer.domElement);
// --- Scene ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a14);
scene.fog = new THREE.Fog(0x0a0a14, 5, 12);
// --- Camera (visitor at the door) ---
const camera = new THREE.PerspectiveCamera(
55, window.innerWidth / window.innerHeight, 0.1, 50
);
camera.position.set(0, 2.0, 4.5);
// --- Build scene elements ---
const { crystalBall, crystalLight, fireLight, candleLights } = buildRoom(scene);
const wizard = createWizard();
scene.add(wizard.group);
const familiar = createFamiliar();
scene.add(familiar.group);
// --- Controls ---
const controls = setupControls(camera, renderer.domElement);
// --- State ---
const stateReader = new StateReader();
const moodEl = document.getElementById("mood-text");
stateReader.onChange((state) => {
if (moodEl) {
moodEl.textContent = state.timmyState.mood;
}
});
stateReader.connect();
// --- About Panel ---
const infoBtn = document.getElementById("info-btn");
const aboutPanel = document.getElementById("about-panel");
const aboutClose = document.getElementById("about-close");
const aboutBackdrop = document.getElementById("about-backdrop");
function openAboutPanel() {
aboutPanel.classList.add("open");
document.body.style.overflow = "hidden";
}
function closeAboutPanel() {
aboutPanel.classList.remove("open");
document.body.style.overflow = "";
}
infoBtn.addEventListener("click", openAboutPanel);
aboutClose.addEventListener("click", closeAboutPanel);
aboutBackdrop.addEventListener("click", closeAboutPanel);
// Close on Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && aboutPanel.classList.contains("open")) {
closeAboutPanel();
}
});
// --- Submit Job Modal ---
const submitJobBtn = document.getElementById("submit-job-btn");
const submitJobModal = document.getElementById("submit-job-modal");
const submitJobClose = document.getElementById("submit-job-close");
const submitJobBackdrop = document.getElementById("submit-job-backdrop");
const cancelJobBtn = document.getElementById("cancel-job-btn");
const submitJobForm = document.getElementById("submit-job-form");
const submitJobSubmit = document.getElementById("submit-job-submit");
const jobTitle = document.getElementById("job-title");
const jobDescription = document.getElementById("job-description");
const titleCharCount = document.getElementById("title-char-count");
const descCharCount = document.getElementById("desc-char-count");
const titleError = document.getElementById("title-error");
const descError = document.getElementById("desc-error");
const descWarning = document.getElementById("desc-warning");
const submitJobSuccess = document.getElementById("submit-job-success");
const submitAnotherBtn = document.getElementById("submit-another-btn");
// Constants
const MAX_TITLE_LENGTH = 200;
const MAX_DESC_LENGTH = 2000;
const TITLE_WARNING_THRESHOLD = 150;
const DESC_WARNING_THRESHOLD = 1800;
function openSubmitJobModal() {
submitJobModal.classList.add("open");
document.body.style.overflow = "hidden";
jobTitle.focus();
validateForm();
}
function closeSubmitJobModal() {
submitJobModal.classList.remove("open");
document.body.style.overflow = "";
// Reset form after animation
setTimeout(() => {
resetForm();
}, 300);
}
function resetForm() {
submitJobForm.reset();
submitJobForm.classList.remove("hidden");
submitJobSuccess.classList.add("hidden");
updateCharCounts();
clearErrors();
validateForm();
}
function clearErrors() {
titleError.textContent = "";
titleError.classList.remove("visible");
descError.textContent = "";
descError.classList.remove("visible");
descWarning.textContent = "";
descWarning.classList.remove("visible");
jobTitle.classList.remove("error");
jobDescription.classList.remove("error");
}
function updateCharCounts() {
const titleLen = jobTitle.value.length;
const descLen = jobDescription.value.length;
titleCharCount.textContent = `${titleLen} / ${MAX_TITLE_LENGTH}`;
descCharCount.textContent = `${descLen} / ${MAX_DESC_LENGTH}`;
// Update color based on thresholds
if (titleLen > MAX_TITLE_LENGTH) {
titleCharCount.classList.add("over-limit");
} else if (titleLen > TITLE_WARNING_THRESHOLD) {
titleCharCount.classList.add("near-limit");
titleCharCount.classList.remove("over-limit");
} else {
titleCharCount.classList.remove("near-limit", "over-limit");
}
if (descLen > MAX_DESC_LENGTH) {
descCharCount.classList.add("over-limit");
} else if (descLen > DESC_WARNING_THRESHOLD) {
descCharCount.classList.add("near-limit");
descCharCount.classList.remove("over-limit");
} else {
descCharCount.classList.remove("near-limit", "over-limit");
}
}
function validateTitle() {
const value = jobTitle.value.trim();
const length = jobTitle.value.length;
if (length > MAX_TITLE_LENGTH) {
titleError.textContent = `Title must be ${MAX_TITLE_LENGTH} characters or less`;
titleError.classList.add("visible");
jobTitle.classList.add("error");
return false;
}
if (value === "") {
titleError.textContent = "Title is required";
titleError.classList.add("visible");
jobTitle.classList.add("error");
return false;
}
titleError.textContent = "";
titleError.classList.remove("visible");
jobTitle.classList.remove("error");
return true;
}
function validateDescription() {
const length = jobDescription.value.length;
if (length > MAX_DESC_LENGTH) {
descError.textContent = `Description must be ${MAX_DESC_LENGTH} characters or less`;
descError.classList.add("visible");
descWarning.textContent = "";
descWarning.classList.remove("visible");
jobDescription.classList.add("error");
return false;
}
// Show warning when near limit
if (length > DESC_WARNING_THRESHOLD && length <= MAX_DESC_LENGTH) {
const remaining = MAX_DESC_LENGTH - length;
descWarning.textContent = `${remaining} characters remaining`;
descWarning.classList.add("visible");
} else {
descWarning.textContent = "";
descWarning.classList.remove("visible");
}
descError.textContent = "";
descError.classList.remove("visible");
jobDescription.classList.remove("error");
return true;
}
function validateForm() {
const titleValid = jobTitle.value.trim() !== "" && jobTitle.value.length <= MAX_TITLE_LENGTH;
const descValid = jobDescription.value.length <= MAX_DESC_LENGTH;
submitJobSubmit.disabled = !(titleValid && descValid);
}
// Event listeners
submitJobBtn.addEventListener("click", openSubmitJobModal);
submitJobClose.addEventListener("click", closeSubmitJobModal);
submitJobBackdrop.addEventListener("click", closeSubmitJobModal);
cancelJobBtn.addEventListener("click", closeSubmitJobModal);
submitAnotherBtn.addEventListener("click", resetForm);
// Input event listeners for real-time validation
jobTitle.addEventListener("input", () => {
updateCharCounts();
validateForm();
if (titleError.classList.contains("visible")) {
validateTitle();
}
});
jobTitle.addEventListener("blur", () => {
if (jobTitle.value.trim() !== "" || titleError.classList.contains("visible")) {
validateTitle();
}
});
jobDescription.addEventListener("input", () => {
updateCharCounts();
validateForm();
if (descError.classList.contains("visible")) {
validateDescription();
}
});
jobDescription.addEventListener("blur", () => {
validateDescription();
});
// Form submission
submitJobForm.addEventListener("submit", async (e) => {
e.preventDefault();
const isTitleValid = validateTitle();
const isDescValid = validateDescription();
if (!isTitleValid || !isDescValid) {
return;
}
// Disable submit button while processing
submitJobSubmit.disabled = true;
submitJobSubmit.textContent = "Submitting...";
const formData = {
title: jobTitle.value.trim(),
description: jobDescription.value.trim(),
priority: document.getElementById("job-priority").value,
submitted_at: new Date().toISOString()
};
try {
// Submit to API
const response = await fetch("/api/tasks", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData)
});
if (response.ok) {
// Show success state
submitJobForm.classList.add("hidden");
submitJobSuccess.classList.remove("hidden");
} else {
const errorData = await response.json().catch(() => ({}));
descError.textContent = errorData.detail || "Failed to submit job. Please try again.";
descError.classList.add("visible");
}
} catch (error) {
// For demo/development, show success even if API fails
submitJobForm.classList.add("hidden");
submitJobSuccess.classList.remove("hidden");
} finally {
submitJobSubmit.disabled = false;
submitJobSubmit.textContent = "Submit Job";
}
});
// Close on Escape key for Submit Job Modal
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && submitJobModal.classList.contains("open")) {
closeSubmitJobModal();
}
});
// --- Resize ---
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- Animation loop ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
// Update scene elements
wizard.update(dt);
familiar.update(dt);
controls.update();
// Crystal ball subtle rotation + pulsing glow
crystalBall.rotation.y += dt * 0.3;
const pulse = 0.3 + Math.sin(Date.now() * 0.002) * 0.15;
crystalLight.intensity = pulse;
crystalBall.material.emissiveIntensity = pulse * 0.5;
// Fireplace flicker
fireLight.intensity = 1.2 + Math.sin(Date.now() * 0.005) * 0.15
+ Math.sin(Date.now() * 0.013) * 0.1;
// Candle flicker — each offset slightly for variety
const now = Date.now();
for (let i = 0; i < candleLights.length; i++) {
candleLights[i].intensity = 0.4
+ Math.sin(now * 0.007 + i * 2.1) * 0.1
+ Math.sin(now * 0.019 + i * 1.3) * 0.05;
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>