Ryanhub - file viewer
filename: static/app.js
branch: main
back to repo
const runBtn      = document.getElementById("run");
const resultsDiv  = document.getElementById("results");
const promptEl    = document.getElementById("prompt");
const charCount   = document.getElementById("char-count");
const resultsMeta = document.getElementById("results-meta");

// Palette cycling for cards
const CARD_ACCENTS = [
  { gradient: "linear-gradient(90deg, #7b74c7, #a09ace)", dot: "#7b74c7" },
  { gradient: "linear-gradient(90deg, #3aaa8a, #5bbfa6)", dot: "#3aaa8a" },
  { gradient: "linear-gradient(90deg, #c49a3a, #d4b46a)", dot: "#c49a3a" },
  { gradient: "linear-gradient(90deg, #b85c8a, #cc85a8)", dot: "#b85c8a" },
  { gradient: "linear-gradient(90deg, #4a7ec7, #7aa3d4)", dot: "#4a7ec7" },
];

// Live character count
promptEl.addEventListener("input", () => {
  const n = promptEl.value.length;
  charCount.textContent = `${n} character${n !== 1 ? "s" : ""}`;
});

// Allow Ctrl+Enter to submit
promptEl.addEventListener("keydown", (e) => {
  if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) runBtn.click();
});

function makeSkeletonCard(modelName, idx) {
  const accent = CARD_ACCENTS[idx % CARD_ACCENTS.length];
  const card = document.createElement("div");
  card.className = "card";
  card.dataset.model = modelName;
  card.style.setProperty("--card-accent", accent.gradient);
  card.style.setProperty("--dot-color", accent.dot);
  card.style.animationDelay = `${idx * 60}ms`;

  card.innerHTML = `
    <div class="card-header">
      <span class="model-name">
        <span class="model-dot"></span>
        ${modelName}
      </span>
      <span class="status-badge loading">Thinking…</span>
    </div>
    <div class="card-output">
      <div class="skeleton skeleton-line"></div>
      <div class="skeleton skeleton-line"></div>
      <div class="skeleton skeleton-line"></div>
      <div class="skeleton skeleton-line"></div>
    </div>
  `;
  return card;
}

function updateCard(card, result) {
  const badge  = card.querySelector(".status-badge");
  const output = card.querySelector(".card-output");

  if (result.error) {
    badge.className  = "status-badge err";
    badge.textContent = "Error";
    output.className  = "card-output error-text";
    output.textContent = result.error;
  } else {
    badge.className  = "status-badge ok";
    badge.textContent = "Done";
    output.className  = "card-output";
    output.textContent = result.output || "(no output)";
  }
}

runBtn.addEventListener("click", async () => {
  const prompt = promptEl.value.trim();
  if (!prompt) {
    promptEl.focus();
    promptEl.style.outline = "2px solid rgba(248,113,113,0.6)";
    setTimeout(() => { promptEl.style.outline = ""; }, 1200);
    return;
  }

  // UI → loading state
  runBtn.disabled = true;
  runBtn.classList.add("loading");
  runBtn.querySelector("svg").innerHTML =
    '<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke-linecap="round"/>';

  resultsMeta.classList.remove("visible");

  // ── Fetch config to know which models to expect ──
  let modelNames = [];
  try {
    const cfg = await fetch("/prompts/motion-description.txt"); // just to warm up; we don't need it here
    // We'll infer model names from the API response instead
  } catch (_) {}

  // Show placeholder skeleton cards (3 by default; replaced on response)
  resultsDiv.innerHTML = "";
  const placeholderCount = 3;
  const placeholders = [];
  for (let i = 0; i < placeholderCount; i++) {
    const card = makeSkeletonCard(`model-${i + 1}`, i);
    resultsDiv.appendChild(card);
    placeholders.push(card);
  }

  const t0 = Date.now();

  try {
    const resp = await fetch("/api/run-all", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt }),
    });

    if (!resp.ok) throw new Error(`Server error: ${resp.status}`);

    const results = await resp.json();
    const elapsed = ((Date.now() - t0) / 1000).toFixed(1);

    // Clear placeholders and render real cards
    resultsDiv.innerHTML = "";

    results.forEach((r, idx) => {
      const accent = CARD_ACCENTS[idx % CARD_ACCENTS.length];
      const card   = document.createElement("div");
      card.className = "card";
      card.style.setProperty("--card-accent", accent.gradient);
      card.style.setProperty("--dot-color", accent.dot);
      card.style.animationDelay = `${idx * 80}ms`;

      const isError = !!r.error;
      card.innerHTML = `
        <div class="card-header">
          <span class="model-name">
            <span class="model-dot"></span>
            ${r.model}
          </span>
          <span class="status-badge ${isError ? "err" : "ok"}">${isError ? "Error" : "Done"}</span>
        </div>
        <div class="card-output ${isError ? "error-text" : ""}">
          ${isError ? r.error : (r.output || "(no output)")}
        </div>
      `;

      resultsDiv.appendChild(card);
    });

    resultsMeta.textContent = `${results.length} model${results.length !== 1 ? "s" : ""} · ${elapsed}s`;
    resultsMeta.classList.add("visible");

  } catch (err) {
    resultsDiv.innerHTML = `
      <div class="card" style="--card-accent: linear-gradient(90deg,#f87171,#ef4444); --dot-color:#f87171; grid-column: 1/-1;">
        <div class="card-header">
          <span class="model-name"><span class="model-dot"></span>Request Failed</span>
          <span class="status-badge err">Error</span>
        </div>
        <div class="card-output error-text">${err.message}</div>
      </div>
    `;
  } finally {
    runBtn.disabled = false;
    runBtn.classList.remove("loading");
    runBtn.querySelector("svg").innerHTML =
      '<polygon points="5 3 19 12 5 21 5 3"/>';
  }
});