| const analyzeForm = document.getElementById("analyze-form"); |
| const chatForm = document.getElementById("chat-form"); |
| const analyzeButton = document.getElementById("analyze-button"); |
| const chatButton = document.getElementById("chat-button"); |
| const videoFile = document.getElementById("video-file"); |
| const fileTrigger = document.getElementById("file-trigger"); |
| const fileName = document.getElementById("file-name"); |
| const languageSelect = document.getElementById("language"); |
| const questionInput = document.getElementById("question"); |
| const statusCard = document.getElementById("status-card"); |
| const statusText = document.getElementById("status-text"); |
| const scoresEl = document.getElementById("scores"); |
| const assessmentEl = document.getElementById("assessment"); |
| const chatLog = document.getElementById("chat-log"); |
| const jobIdEl = document.getElementById("job-id"); |
|
|
| let activeJobId = null; |
| let pollHandle = null; |
|
|
| function renderSelectedFile() { |
| const selected = videoFile.files && videoFile.files.length ? videoFile.files[0].name : "No file chosen"; |
| fileName.textContent = selected; |
| } |
|
|
| function setStatus(kind, text) { |
| statusCard.className = `status-card ${kind}`; |
| statusText.textContent = text; |
| } |
|
|
| function resetResults() { |
| if (pollHandle) { |
| clearTimeout(pollHandle); |
| pollHandle = null; |
| } |
| activeJobId = null; |
| jobIdEl.textContent = ""; |
| scoresEl.innerHTML = ""; |
| assessmentEl.textContent = "Visible after analysis completes."; |
| assessmentEl.className = "answer-box empty"; |
| chatLog.innerHTML = '<div class="message message-system">Once analysis finishes, you can continue the conversation with SEMA.</div>'; |
| questionInput.disabled = true; |
| chatButton.disabled = true; |
| } |
|
|
| function renderScores(scores) { |
| const entries = [ |
| ["Overall", scores.total], |
| ["Head", scores.head], |
| ["Hand", scores.hand], |
| ["Torso", scores.torso], |
| ["Foot", scores.foot], |
| ["Arm", scores.arm], |
| ]; |
| scoresEl.innerHTML = entries.map(([label, value]) => ` |
| <div class="score-card"> |
| <span>${label}</span> |
| <strong>${value ?? "-"}</strong> |
| </div> |
| `).join(""); |
| } |
|
|
| function appendMessage(role, content) { |
| const node = document.createElement("div"); |
| node.className = `message message-${role}`; |
| node.textContent = content; |
| chatLog.appendChild(node); |
| chatLog.scrollTop = chatLog.scrollHeight; |
| } |
|
|
| function renderCompleted(job) { |
| activeJobId = job.job_id; |
| jobIdEl.textContent = job.job_id; |
| renderScores(job.result.scores); |
| assessmentEl.textContent = job.result.assessment_text; |
| assessmentEl.className = "answer-box"; |
| questionInput.disabled = false; |
| chatButton.disabled = false; |
| } |
|
|
| function renderAnalysisFailure(message) { |
| scoresEl.innerHTML = ""; |
| assessmentEl.textContent = message; |
| assessmentEl.className = "answer-box"; |
| chatLog.innerHTML = '<div class="message message-system">Analysis failed. Fix the issue above and retry the upload.</div>'; |
| questionInput.disabled = true; |
| chatButton.disabled = true; |
| } |
|
|
| fileTrigger.addEventListener("click", () => { |
| videoFile.click(); |
| }); |
|
|
| videoFile.addEventListener("change", () => { |
| renderSelectedFile(); |
| }); |
|
|
| renderSelectedFile(); |
|
|
| async function pollJob(jobId) { |
| if (pollHandle) { |
| clearTimeout(pollHandle); |
| } |
| try { |
| const response = await fetch(`/api/jobs/${jobId}`); |
| const job = await response.json(); |
| if (!response.ok) { |
| throw new Error(job.detail || "Failed to poll job status."); |
| } |
|
|
| if (job.status === "queued" || job.status === "running") { |
| const fallbackText = job.status === "queued" |
| ? "Job created. Waiting to start." |
| : "Analyzing video. Please wait."; |
| setStatus("running", job.status_message || fallbackText); |
| pollHandle = setTimeout(() => pollJob(jobId), 2500); |
| return; |
| } |
|
|
| if (job.status === "failed") { |
| const errorMessage = job.error || "Analysis failed."; |
| renderAnalysisFailure(errorMessage); |
| setStatus("error", errorMessage); |
| return; |
| } |
|
|
| renderCompleted(job); |
| setStatus("success", "Analysis complete. You can ask follow-up questions."); |
| pollHandle = null; |
| } catch (error) { |
| const errorMessage = error.message || "Analysis job failed."; |
| renderAnalysisFailure(errorMessage); |
| setStatus("error", errorMessage); |
| pollHandle = null; |
| } |
| } |
|
|
| analyzeForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| if (!videoFile.files.length) { |
| setStatus("error", "Please choose a video file first."); |
| return; |
| } |
|
|
| resetResults(); |
| analyzeButton.disabled = true; |
| setStatus("running", "Uploading file and creating job."); |
|
|
| const formData = new FormData(); |
| formData.append("file", videoFile.files[0]); |
| formData.append("language", languageSelect.value); |
|
|
| try { |
| const response = await fetch("/api/jobs", { |
| method: "POST", |
| body: formData, |
| }); |
| const payload = await response.json(); |
| if (!response.ok) { |
| throw new Error(payload.detail || "Failed to create analysis job."); |
| } |
| setStatus("running", payload.status_message || "Upload complete. The job is now queued."); |
| pollJob(payload.job_id); |
| } catch (error) { |
| setStatus("error", error.message || "Upload failed."); |
| } finally { |
| analyzeButton.disabled = false; |
| } |
| }); |
|
|
| chatForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| const question = questionInput.value.trim(); |
| if (!activeJobId) { |
| setStatus("error", "Complete the video analysis first."); |
| return; |
| } |
| if (!question) { |
| setStatus("error", "Please enter a question."); |
| return; |
| } |
|
|
| appendMessage("user", question); |
| questionInput.value = ""; |
| questionInput.disabled = true; |
| chatButton.disabled = true; |
| setStatus("running", "SEMA is generating an answer."); |
|
|
| try { |
| const response = await fetch(`/api/jobs/${activeJobId}/chat`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ question }), |
| }); |
| const payload = await response.json(); |
| if (!response.ok) { |
| throw new Error(payload.detail || "Follow-up answer failed."); |
| } |
| appendMessage("assistant", payload.answer); |
| setStatus("success", "Answer complete. You can keep asking follow-up questions."); |
| } catch (error) { |
| appendMessage("assistant", `Answer failed: ${error.message || "Unknown error"}`); |
| setStatus("error", error.message || "Follow-up answer failed."); |
| } finally { |
| questionInput.disabled = false; |
| chatButton.disabled = false; |
| questionInput.focus(); |
| } |
| }); |
|
|