diff --git a/static/world/index.html b/static/world/index.html index d10f8663..8788afe8 100644 --- a/static/world/index.html +++ b/static/world/index.html @@ -20,11 +20,76 @@ +
+ +
+
+ +

Submit Job

+

Create a task for Timmy and the agent swarm

+ +
+
+ + +
0 / 200
+
+
+ +
+ + +
0 / 2000
+
+
+
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
+
@@ -146,6 +211,244 @@ } }); + // --- 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; diff --git a/static/world/style.css b/static/world/style.css index 40626007..914c355e 100644 --- a/static/world/style.css +++ b/static/world/style.css @@ -263,6 +263,347 @@ canvas { opacity: 1; } +/* Submit Job Button */ +.submit-job-button { + position: absolute; + top: 14px; + right: 72px; + height: 28px; + padding: 0 12px; + background: rgba(10, 10, 20, 0.7); + border: 1px solid rgba(0, 180, 80, 0.4); + border-radius: 14px; + color: #00b450; + cursor: pointer; + pointer-events: auto; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; + font-family: "Courier New", monospace; + font-size: 12px; +} + +.submit-job-button:hover { + background: rgba(0, 180, 80, 0.15); + border-color: rgba(0, 180, 80, 0.7); + transform: scale(1.05); +} + +.submit-job-button svg { + width: 14px; + height: 14px; +} + +/* Submit Job Modal */ +.submit-job-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + pointer-events: none; + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.submit-job-modal.open { + pointer-events: auto; + visibility: visible; + opacity: 1; +} + +.submit-job-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.95); + width: 480px; + max-width: 90%; + max-height: 90vh; + background: rgba(10, 10, 20, 0.98); + border: 1px solid rgba(218, 165, 32, 0.3); + border-radius: 12px; + padding: 32px; + overflow-y: auto; + transition: transform 0.3s ease; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); +} + +.submit-job-modal.open .submit-job-content { + transform: translate(-50%, -50%) scale(1); +} + +.submit-job-close { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + height: 32px; + padding: 0; + background: transparent; + border: 1px solid rgba(160, 160, 160, 0.3); + border-radius: 50%; + color: #aaa; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.submit-job-close:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(218, 165, 32, 0.5); + color: #daa520; +} + +.submit-job-close svg { + width: 18px; + height: 18px; +} + +.submit-job-content h2 { + font-size: 22px; + color: #daa520; + margin: 0 0 8px 0; + font-weight: 600; +} + +.submit-job-subtitle { + font-size: 13px; + color: #888; + margin: 0 0 24px 0; +} + +/* Form Styles */ +.submit-job-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.submit-job-form.hidden { + display: none; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-size: 13px; + color: #ccc; + font-weight: 500; +} + +.form-group label .required { + color: #ff4444; + margin-left: 4px; +} + +.form-group input, +.form-group textarea, +.form-group select { + background: rgba(30, 30, 40, 0.8); + border: 1px solid rgba(160, 160, 160, 0.3); + border-radius: 6px; + padding: 10px 12px; + color: #e0e0e0; + font-family: "Courier New", monospace; + font-size: 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: rgba(218, 165, 32, 0.6); + box-shadow: 0 0 0 2px rgba(218, 165, 32, 0.1); +} + +.form-group input.error, +.form-group textarea.error { + border-color: #ff4444; + box-shadow: 0 0 0 2px rgba(255, 68, 68, 0.1); +} + +.form-group input::placeholder, +.form-group textarea::placeholder { + color: #666; +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.form-group select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 36px; +} + +.form-group select option { + background: #1a1a2e; + color: #e0e0e0; +} + +/* Character Count */ +.char-count { + font-size: 11px; + color: #666; + text-align: right; + margin-top: 4px; + transition: color 0.2s ease; +} + +.char-count.near-limit { + color: #ffaa33; +} + +.char-count.over-limit { + color: #ff4444; + font-weight: bold; +} + +/* Validation Messages */ +.validation-error { + font-size: 12px; + color: #ff4444; + margin-top: 4px; + min-height: 16px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.validation-error.visible { + opacity: 1; +} + +.validation-warning { + font-size: 12px; + color: #ffaa33; + margin-top: 4px; + min-height: 16px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.validation-warning.visible { + opacity: 1; +} + +/* Action Buttons */ +.submit-job-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 8px; +} + +.btn-secondary { + padding: 10px 20px; + background: transparent; + border: 1px solid rgba(160, 160, 160, 0.4); + border-radius: 6px; + color: #aaa; + font-family: "Courier New", monospace; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(160, 160, 160, 0.6); + color: #ccc; +} + +.btn-primary { + padding: 10px 20px; + background: linear-gradient(135deg, rgba(0, 180, 80, 0.8), rgba(0, 140, 60, 0.9)); + border: 1px solid rgba(0, 180, 80, 0.5); + border-radius: 6px; + color: #fff; + font-family: "Courier New", monospace; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(0, 200, 90, 0.9), rgba(0, 160, 70, 1)); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 180, 80, 0.3); +} + +.btn-primary:disabled { + background: rgba(100, 100, 100, 0.3); + border-color: rgba(100, 100, 100, 0.3); + color: #666; + cursor: not-allowed; +} + +/* Success State */ +.submit-job-success { + text-align: center; + padding: 32px 16px; +} + +.submit-job-success.hidden { + display: none; +} + +.success-icon { + width: 64px; + height: 64px; + margin: 0 auto 20px; + color: #00b450; +} + +.success-icon svg { + width: 100%; + height: 100%; +} + +.submit-job-success h3 { + font-size: 20px; + color: #00b450; + margin: 0 0 12px 0; +} + +.submit-job-success p { + font-size: 14px; + color: #888; + margin: 0 0 24px 0; + line-height: 1.5; +} + +/* Backdrop */ +.submit-job-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + opacity: 0; + transition: opacity 0.3s ease; +} + +.submit-job-modal.open .submit-job-backdrop { + opacity: 1; +} + /* Mobile adjustments */ @media (max-width: 480px) { .about-panel-content { @@ -281,4 +622,34 @@ canvas { width: 14px; height: 14px; } + + .submit-job-button { + right: 64px; + height: 26px; + padding: 0 10px; + font-size: 11px; + } + + .submit-job-button svg { + width: 12px; + height: 12px; + } + + .submit-job-content { + width: 95%; + padding: 24px 20px; + } + + .submit-job-content h2 { + font-size: 20px; + } + + .submit-job-actions { + flex-direction: column-reverse; + } + + .btn-secondary, + .btn-primary { + width: 100%; + } }