taskdetail.html 5.94 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>任务详情 · MiniMax</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar">
  <h1>MiniMax 视频生成测试台</h1>
  <div class="spacer"></div>
  <label class="small">fal API Key</label>
  <input type="password" id="apiKeyInput" placeholder="key_id:key_secret" />
  <button class="secondary" id="saveKeyBtn">保存</button>
</header>

<div class="layout">
  <aside class="sidebar">
    <div class="toolbar">
      <input type="search" id="searchInput" placeholder="搜索 prompt..." />
      <select id="statusFilter">
        <option value="">全部状态</option>
        <option value="Processing">生成中</option>
        <option value="succeeded">成功</option>
        <option value="failed">失败</option>
        <option value="cancelled">取消</option>
      </select>
      <select id="modelFilter">
        <option value="">全部模型</option>
        <option value="MiniMax-H3">MiniMax-H3</option>
        <option value="MiniMax-H3-Max">MiniMax-H3-Max</option>
      </select>
      <select id="sortSelect">
        <option value="created_desc">创建时间 ↓</option>
        <option value="created_asc">创建时间 ↑</option>
        <option value="elapsed_asc">耗时 ↑</option>
        <option value="elapsed_desc">耗时 ↓</option>
      </select>
      <button id="newTaskBtn">+ 新建任务</button>
    </div>
    <div class="task-list" id="taskList"></div>
  </aside>

  <main class="main">
    <div id="body"></div>
  </main>
</div>

<script src="app.js"></script>
<script>
const apiKeyInput = document.getElementById("apiKeyInput");
apiKeyInput.value = getApiKey();
document.getElementById("saveKeyBtn").onclick = (e) => {
  setApiKey(apiKeyInput.value);
  const btn = e.currentTarget;
  const old = btn.textContent;
  btn.textContent = "已保存 ✓";
  setTimeout(() => (btn.textContent = old), 1200);
};

const params = new URLSearchParams(location.search);
const taskId = params.get("id");
let pollTimer = null;
let tickTimer = null;

initSidebar(taskId);

function render() {
  const task = getTask(taskId);
  const body = document.getElementById("body");
  if (!task) {
    body.innerHTML = `<div class="empty-hint">任务不存在,可能已被删除。</div>`;
    return;
  }

  const elapsedNow = task.submittedAt
    ? (task.completedAt || Date.now()) - task.submittedAt
    : null;

  body.innerHTML = `
    <div class="card">
      <div class="detail-header">
        <div>
          <h2>${escapeHtml(task.prompt.slice(0, 60))}${task.prompt.length > 60 ? "..." : ""}</h2>
          <span class="badge ${task.status}">${statusLabel(task.status)}</span>
        </div>
        <div class="detail-actions">
          <button class="danger" id="deleteBtn">删除</button>
        </div>
      </div>

      <dl class="kv-grid">
        <dt>模型</dt><dd>${task.model}(fal: ${task.falModel})</dd>
        <dt>分辨率 / 时长 / 比例</dt><dd>${task.resolution} · ${task.duration}s · ${task.ratio}</dd>
        <dt>fal request_id</dt><dd>${task.requestId || "-"}</dd>
        <dt>创建时间</dt><dd>${fmtDate(task.createdAt)}</dd>
        <dt>提交时间</dt><dd>${fmtDate(task.submittedAt)}</dd>
        <dt>完成时间</dt><dd>${fmtDate(task.completedAt)}</dd>
        <dt>耗时(生成速度)</dt><dd id="elapsedVal">${elapsedNow != null ? fmtElapsed(elapsedNow) : "-"}</dd>
      </dl>

      <div class="field" style="margin-top:14px;">
        <label>Prompt</label>
        <div>${escapeHtml(task.prompt)}</div>
      </div>

      ${task.status === "succeeded" && task.videoUrl ? `
        <div class="field">
          <label>生成结果</label>
          <video src="${task.videoUrl}" controls></video>
          <div class="hint"><a href="${task.videoUrl}" target="_blank">在新标签页打开 / 下载</a></div>
        </div>` : ""}

      ${task.error ? `<div class="error-box">${escapeHtml(typeof task.error === "string" ? task.error : JSON.stringify(task.error, null, 2))}</div>` : ""}

      ${task.status === "Processing" ? `<p class="hint">自动轮询任务状态中${task.queuePosition != null ? `(队列位置 ${task.queuePosition})` : ""}...</p>` : ""}

      ${task.lastRaw ? `
        <details class="raw">
          <summary>原始接口返回(调试用)</summary>
          <pre>${escapeHtml(JSON.stringify(task.lastRaw, null, 2))}</pre>
        </details>` : ""}
    </div>
  `;

  document.getElementById("deleteBtn").addEventListener("click", () => {
    if (confirm("确认删除该任务?")) {
      deleteTask(task.id);
      location.href = "index.html";
    }
  });
}

async function poll() {
  const task = getTask(taskId);
  if (!task || task.status !== "Processing" || !task.statusUrl) return;
  try {
    const statusJson = await queryVideoStatus(task);
    task.lastRaw = statusJson;
    task.queuePosition = statusJson.queue_position ?? null;
    if (statusJson.status === "COMPLETED") {
      const result = await fetchVideoResult(task);
      task.lastRaw = result;
      task.status = "succeeded";
      task.videoUrl = result.video?.url || null;
      task.completedAt = Date.now();
    }
    // IN_QUEUE / IN_PROGRESS: nothing to update besides lastRaw, keep polling.
    upsertTask(task);
    render();
    renderSidebar(taskId);
  } catch (err) {
    task.status = "failed";
    task.error = err.body || err.message;
    task.completedAt = Date.now();
    upsertTask(task);
    render();
    renderSidebar(taskId);
  }
  if (task.status === "Processing") {
    pollTimer = setTimeout(poll, 4000);
  }
}

function tick() {
  const task = getTask(taskId);
  if (task && task.status === "Processing" && task.submittedAt) {
    const el = document.getElementById("elapsedVal");
    if (el) el.textContent = fmtElapsed(Date.now() - task.submittedAt);
  }
}

render();
poll();
tickTimer = setInterval(tick, 1000);
window.addEventListener("beforeunload", () => {
  clearTimeout(pollTimer);
  clearInterval(tickTimer);
});
</script>
</body>
</html>