from __future__ import annotations

import html

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from ..layout.page import page


def register(app: FastAPI) -> None:
    @app.get("/ui/tasks/new")
    async def ui_new_task() -> HTMLResponse:
        ppo = """# PPO TaskSpec (YAML)
workload: ppo  # 任务类型（必填）：ppo｜grpo｜sft
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        grpo = """# GRPO TaskSpec (YAML)
workload: grpo  # 任务类型（必填）：ppo｜grpo｜sft（grpo 会自动启用对应的算法配置）
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        sft = """# SFT TaskSpec (YAML)
workload: sft  # 任务类型（必填）：ppo｜grpo｜sft
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k_sft/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k_sft/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2；单机可设 1）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4；单卡可设 1）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# trainer_device: cpu  # 仅 SFT 生效：driver 侧 device（可选，默认：cpu）
# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        adv = """# Advanced TaskSpec (YAML) - v3.6
kind: advanced  # 任务类型（必填）：advanced（自定义 command）
# 说明：平台统一按 "advanced" 做任务分类与 task_id 命名（不按 ppo/grpo/sft 细分）。
#
# 自定义训练命令：平台会做 $HOME 宏替换：
# - $HOME -> /private/users/<user>
# - $HOME/common/datasets -> /private/datasets（共享只读数据）
# - $HOME/common/hf -> /private/hf（共享只读 HF cache）
#
# W&B（v3.6）：
# - 平台会在 runtime_env 注入 WANDB_BASE_URL/WANDB_API_KEY/WANDB_DIR
# - 以及注入以下 env，供 Advanced command 使用（无需用户手动修改 project）：
#   - MVP_TRAINER_LOGGER: "console" 或 "[console,wandb]"
#   - MVP_WANDB_PROJECT: "<user_id>_project"
#   - MVP_WANDB_RUN: "<ray_submission_id>"
#
nnodes: 2  # 训练节点数（必填）：用于平台队列调度与资源预检查
n_gpus_per_node: 4  # 每节点 GPU 数（必填）：用于平台队列调度与资源预检查

command: |
  PYTHONUNBUFFERED=1 \
  python3 -m verl.trainer.main_ppo \
    data.train_files=$HOME/common/datasets/gsm8k/train.parquet \
    data.val_files=$HOME/common/datasets/gsm8k/test.parquet \
    data.train_batch_size=256 \
    data.max_prompt_length=512 \
    data.max_response_length=512 \
    actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \
    actor_rollout_ref.actor.optim.lr=1e-6 \
    actor_rollout_ref.actor.ppo_mini_batch_size=64 \
    actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \
    actor_rollout_ref.rollout.name=vllm \
    actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \
    actor_rollout_ref.rollout.tensor_model_parallel_size=1 \
    actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \
    actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \
    critic.optim.lr=1e-5 \
    critic.model.path=Qwen/Qwen2.5-0.5B-Instruct \
    critic.ppo_micro_batch_size_per_gpu=4 \
    algorithm.kl_ctrl.kl_coef=0.001 \
    trainer.logger=${MVP_TRAINER_LOGGER} \
    trainer.project_name=${MVP_WANDB_PROJECT} \
    trainer.experiment_name=${MVP_WANDB_RUN} \
    trainer.val_before_train=False \
    trainer.nnodes=2 \
    trainer.n_gpus_per_node=4 \
    trainer.total_epochs=1 \
    trainer.total_training_steps=10 \
    trainer.save_freq=10 \
    trainer.test_freq=-1 \
    trainer.resume_mode=disable \
    trainer.default_local_dir=checkpoints \
    +ray_kwargs.ray_init.address=auto \
    hydra.run.dir=logs/hydra

# 可选：自定义 reward（方式 A：直接写在 command 里）
# command 里增加如下 overrides：
#   custom_reward_function.path=$HOME/code/reward.py
#   custom_reward_function.name=compute_score
""".strip()
        merge = """# Model Merge (YAML) - v3.5 (Advanced command)
# 用途：将 VERL 训练产生的 FSDP 分片 checkpoint 合并为 HuggingFace 格式目录。
#
# 你需要把 <RAY_SUBMISSION_ID> 和 <GLOBAL_STEP> 替换成真实值：
# - submission id：在 Tasks 详情页里看到的 `ray_submission_id`（如 mvp2-...--a01）
# - global_step：对应 checkpoints 下的 global_step_xxx 目录（如 global_step_10）
#
# 注意：这里不需要 GPU，建议把 n_gpus_per_node 设为 0，这样不受训练任务占用 GPU 的影响。
kind: advanced
nnodes: 1
n_gpus_per_node: 0

command: |
  python3 -m verl.model_merger merge \
    --backend fsdp \
    --local_dir $HOME/jobs/<RAY_SUBMISSION_ID>/checkpoints/<GLOBAL_STEP>/actor \
    --target_dir $HOME/jobs/<RAY_SUBMISSION_ID>/checkpoints/<GLOBAL_STEP>/actor/huggingface
""".strip()
        evaluation = """# Evaluation (YAML) - v3.6 (Advanced command)
# 用途：对“已生成的结果 parquet”做离线评估（基于 custom reward function），示例使用 VERL 内置 main_eval。
#
# 说明：
# - 你需要准备一个包含生成 responses 的 parquet，并替换 <EVAL_PARQUET>（示例放在某个 job 目录下）。
# - main_eval 会在 Ray 上并发计算，因此这里也用 +ray_kwargs 连接已有 Ray 集群。
#
kind: advanced
nnodes: 1
n_gpus_per_node: 0

command: |
  PYTHONUNBUFFERED=1 \
  python3 -m verl.trainer.main_eval \
    data.path=$HOME/jobs/<RAY_SUBMISSION_ID>/outputs/<EVAL_PARQUET>.parquet \
    +ray_kwargs.ray_init.address=auto

# 可选：指定 parquet schema（按你的 parquet 列名调整）
#   data.response_key=responses
#   data.data_source_key=data_source
#   data.reward_model_key=reward_model
#
# 可选：自定义 reward（方式 A：用户自行提供 reward.py）
#   custom_reward_function.path=$HOME/code/reward.py
#   custom_reward_function.name=compute_score
""".strip()
        body = f"""
<h1>New Task</h1>
<div class="card">
  <div class="muted">
    Paste TaskSpec YAML and submit to API server.
    Basic tasks require <code>code_path</code>; Advanced tasks use <code>kind: advanced</code> with a custom <code>command</code>.
  </div>
  <div style="height:10px"></div>
  <div class="row">
    <button class="btn" id="mode-yaml">YAML 模式</button>
    <button class="btn" id="mode-form">表单模式</button>
  </div>
  <div style="height:10px"></div>

  <div id="panel-yaml">
  <div class="row">
    <button class="btn" id="tpl-ppo">PPO example</button>
    <button class="btn" id="tpl-grpo">GRPO example</button>
    <button class="btn" id="tpl-sft">SFT example</button>
    <button class="btn" id="tpl-adv">Advanced example</button>
    <button class="btn" id="tpl-eval">Evaluation example</button>
    <button class="btn" id="tpl-merge">Model merge example</button>
  </div>
  <div style="height:10px"></div>
  <textarea id="yaml" rows="16">{html.escape(ppo)}</textarea>
  </div>

  <div id="panel-form" style="display:none">
    <div class="row">
      <div style="min-width:240px">
        <div class="muted">模板</div>
        <div style="height:6px"></div>
        <select id="f-tpl" style="width:240px; padding:10px 12px; border-radius: 12px; background: rgba(255,255,255,0.06); color: var(--fg); border: 1px solid rgba(255,255,255,0.12);">
          <option value="ppo">PPO</option>
          <option value="grpo">GRPO</option>
          <option value="sft">SFT</option>
          <option value="advanced">Advanced</option>
          <option value="merge">Model Merge</option>
        </select>
      </div>
      <div class="muted" style="flex:1; min-width:260px">
        表单模式会生成 TaskSpec YAML 并用同一个 API 提交；可随时切回 YAML 模式查看/编辑生成结果。
      </div>
    </div>

    <div style="height:14px"></div>

    <div id="f-basic">
      <div class="row">
        <div style="flex:1; min-width:260px">
          <div class="muted">code_path</div>
          <div style="height:6px"></div>
          <input id="f-code-path" />
        </div>
        <div style="flex:1; min-width:260px">
          <div class="muted">model_id</div>
          <div style="height:6px"></div>
          <input id="f-model-id" />
        </div>
      </div>
      <div style="height:10px"></div>
      <div class="row">
        <div style="flex:1; min-width:260px">
          <div class="muted">train_file</div>
          <div style="height:6px"></div>
          <input id="f-train-file" />
        </div>
        <div style="flex:1; min-width:260px">
          <div class="muted">val_file</div>
          <div style="height:6px"></div>
          <input id="f-val-file" />
        </div>
      </div>
      <div style="height:10px"></div>
      <div class="row">
        <div style="min-width:220px">
          <div class="muted">nnodes</div>
          <div style="height:6px"></div>
          <div class="row" style="gap:8px">
            <button class="btn" id="f-nnodes-dec">-</button>
            <input id="f-nnodes" type="number" min="1" step="1" style="width:120px" />
            <button class="btn" id="f-nnodes-inc">+</button>
          </div>
        </div>
        <div style="min-width:240px">
          <div class="muted">n_gpus_per_node</div>
          <div style="height:6px"></div>
          <div class="row" style="gap:8px">
            <button class="btn" id="f-gpn-dec">-</button>
            <input id="f-gpn" type="number" min="0" step="1" style="width:120px" />
            <button class="btn" id="f-gpn-inc">+</button>
          </div>
        </div>
        <div style="min-width:220px">
          <div class="muted">total_epochs</div>
          <div style="height:6px"></div>
          <div class="row" style="gap:8px">
            <button class="btn" id="f-epochs-dec">-</button>
            <input id="f-epochs" type="number" min="1" step="1" style="width:120px" />
            <button class="btn" id="f-epochs-inc">+</button>
          </div>
        </div>
        <div style="min-width:260px">
          <div class="muted">total_training_steps（可选）</div>
          <div style="height:6px"></div>
          <input id="f-steps" type="number" min="1" step="1" placeholder="留空表示不传（让 VERL 自动推导）" />
        </div>
      </div>
      <div style="height:10px"></div>
      <div class="row">
        <div style="min-width:220px">
          <div class="muted">save_freq</div>
          <div style="height:6px"></div>
          <input id="f-save-freq" type="number" min="1" step="1" style="width:160px" />
        </div>
        <div style="min-width:260px">
          <div class="muted">test_freq（可选）</div>
          <div style="height:6px"></div>
          <input id="f-test-freq" type="number" min="1" step="1" placeholder="留空表示不传（训练端会当成 -1=不验证）" />
        </div>
        <div id="f-sft-device-wrap" style="min-width:260px; display:none">
          <div class="muted">trainer_device（仅 SFT）</div>
          <div style="height:6px"></div>
          <select id="f-trainer-device" style="width:200px; padding:10px 12px; border-radius: 12px; background: rgba(255,255,255,0.06); color: var(--fg); border: 1px solid rgba(255,255,255,0.12);">
            <option value="cpu">cpu</option>
            <option value="cuda">cuda</option>
          </select>
        </div>
      </div>
    </div>

    <div id="f-adv" style="display:none">
      <div class="row">
        <div style="min-width:220px">
          <div class="muted">nnodes</div>
          <div style="height:6px"></div>
          <div class="row" style="gap:8px">
            <button class="btn" id="f-adv-nnodes-dec">-</button>
            <input id="f-adv-nnodes" type="number" min="1" step="1" style="width:120px" />
            <button class="btn" id="f-adv-nnodes-inc">+</button>
          </div>
        </div>
        <div style="min-width:240px">
          <div class="muted">n_gpus_per_node</div>
          <div style="height:6px"></div>
          <div class="row" style="gap:8px">
            <button class="btn" id="f-adv-gpn-dec">-</button>
            <input id="f-adv-gpn" type="number" min="0" step="1" style="width:120px" />
            <button class="btn" id="f-adv-gpn-inc">+</button>
          </div>
        </div>
      </div>
      <div style="height:10px"></div>
      <div class="muted">command</div>
      <div style="height:6px"></div>
      <textarea id="f-command" rows="10"></textarea>
      <div style="height:10px"></div>
      <div class="muted">
        Tips:
        Advanced 模式只允许执行 <code>python3 -m verl.*</code>。
        如果你需要自定义 reward，可把 <code>reward.py</code> 上传到 <code>$HOME/code/</code> 并在 command 里引用。
      </div>
    </div>
  </div>

  <div style="height:10px"></div>
  <div class="row">
    <button class="btn" id="submit">Submit</button>
    <a class="btn" href="/ui/tasks" style="display:inline-block">Back</a>
  </div>
  <div style="height:10px"></div>
  <pre id="msg" class="muted"></pre>
</div>
""".strip()

        tpl_ppo = ppo.replace("\n", "\\n").replace('"', '\\"')
        tpl_grpo = grpo.replace("\n", "\\n").replace('"', '\\"')
        tpl_sft = sft.replace("\n", "\\n").replace('"', '\\"')
        tpl_adv = adv.replace("\n", "\\n").replace('"', '\\"')
        tpl_eval = evaluation.replace("\n", "\\n").replace('"', '\\"')
        tpl_merge = merge.replace("\n", "\\n").replace('"', '\\"')

        script = (
            """
document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265);
const yamlEl = document.getElementById("yaml");
const msg = document.getElementById("msg");

const TPL_PPO = "__TPL_PPO__";
const TPL_GRPO = "__TPL_GRPO__";
const TPL_SFT = "__TPL_SFT__";
const TPL_ADV = "__TPL_ADV__";
const TPL_EVAL = "__TPL_EVAL__";
const TPL_MERGE = "__TPL_MERGE__";

function showMode(mode) {
  const a = document.getElementById("mode-yaml");
  const b = document.getElementById("mode-form");
  const py = document.getElementById("panel-yaml");
  const pf = document.getElementById("panel-form");
  if (mode === "yaml") {
    a.classList.add("active"); b.classList.remove("active");
    py.style.display = ""; pf.style.display = "none";
  } else {
    b.classList.add("active"); a.classList.remove("active");
    pf.style.display = ""; py.style.display = "none";
  }
}

function yamlQuote(v) {
  v = String(v || "");
  if (!v) return '""';
  if (/^[A-Za-z0-9_./:-]+$/.test(v) && !v.includes("#")) return v;
  return '"' + v.replaceAll("\\\\","\\\\\\\\").replaceAll('"','\\\\"') + '"';
}

function updateFormVisibility() {
  const tpl = document.getElementById("f-tpl").value;
  const basic = document.getElementById("f-basic");
  const adv = document.getElementById("f-adv");
  const sftDev = document.getElementById("f-sft-device-wrap");
  if (tpl === "advanced" || tpl === "merge") {
    basic.style.display = "none";
    adv.style.display = "";
    sftDev.style.display = "none";
  } else {
    basic.style.display = "";
    adv.style.display = "none";
    sftDev.style.display = (tpl === "sft") ? "" : "none";
  }
}

function updatePreview() {
  const tpl = document.getElementById("f-tpl").value;
  if (tpl === "advanced" || tpl === "merge") {
    const nnodes = Number(document.getElementById("f-adv-nnodes").value || "1") || 1;
    const gpn = Number(document.getElementById("f-adv-gpn").value || "0") || 0;
    const cmd = (document.getElementById("f-command").value || "").trimEnd();
    const out = `kind: advanced\\n` +
      `nnodes: ${nnodes}\\n` +
      `n_gpus_per_node: ${gpn}\\n\\n` +
      `command: |\\n` +
      cmd.split("\\n").map(line => "  " + line).join("\\n") + "\\n";
    return out;
  }

  const workload = tpl;
  const codePath = document.getElementById("f-code-path").value || "";
  const modelId = document.getElementById("f-model-id").value || "";
  const trainFile = document.getElementById("f-train-file").value || "";
  const valFile = document.getElementById("f-val-file").value || "";
  const nnodes = Number(document.getElementById("f-nnodes").value || "2") || 2;
  const gpn = Number(document.getElementById("f-gpn").value || "4") || 4;
  const epochs = Number(document.getElementById("f-epochs").value || "1") || 1;
  const steps = (document.getElementById("f-steps").value || "").trim();
  const saveFreq = Number(document.getElementById("f-save-freq").value || "10") || 10;
  const testFreq = (document.getElementById("f-test-freq").value || "").trim();
  const trainerDevice = document.getElementById("f-trainer-device").value || "cpu";

  let y = "";
  y += `workload: ${workload}\\n`;
  y += `code_path: ${yamlQuote(codePath)}\\n`;
  y += `model_id: ${yamlQuote(modelId)}\\n`;
  y += `train_file: ${yamlQuote(trainFile)}\\n`;
  y += `val_file: ${yamlQuote(valFile)}\\n`;
  y += `nnodes: ${nnodes}\\n`;
  y += `n_gpus_per_node: ${gpn}\\n`;
  y += `total_epochs: ${epochs}\\n`;
  if (steps) y += `total_training_steps: ${Number(steps)}\\n`;
  y += `save_freq: ${saveFreq}\\n`;
  if (testFreq) y += `test_freq: ${Number(testFreq)}\\n`;
  if (workload === "sft") y += `trainer_device: ${trainerDevice}\\n`;
  return y;
}

function bindStepper(inputId, decId, incId, min) {
  const inp = document.getElementById(inputId);
  document.getElementById(decId).onclick = () => { inp.value = String(Math.max(min, (Number(inp.value || "0") || 0) - 1)); inp.dispatchEvent(new Event("input")); };
  document.getElementById(incId).onclick = () => { inp.value = String((Number(inp.value || "0") || 0) + 1); inp.dispatchEvent(new Event("input")); };
}

document.getElementById("tpl-ppo").onclick = () => { yamlEl.value = TPL_PPO; };
document.getElementById("tpl-grpo").onclick = () => { yamlEl.value = TPL_GRPO; };
document.getElementById("tpl-sft").onclick = () => { yamlEl.value = TPL_SFT; };
document.getElementById("tpl-adv").onclick = () => { yamlEl.value = TPL_ADV; };
document.getElementById("tpl-eval").onclick = () => { yamlEl.value = TPL_EVAL; };
document.getElementById("tpl-merge").onclick = () => { yamlEl.value = TPL_MERGE; };

document.getElementById("f-code-path").value = "/private/common/code/verl/verl_repo";
document.getElementById("f-model-id").value = "Qwen/Qwen2.5-0.5B-Instruct";
document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet";
document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet";
document.getElementById("f-nnodes").value = "2";
document.getElementById("f-gpn").value = "4";
document.getElementById("f-epochs").value = "1";
document.getElementById("f-save-freq").value = "10";
document.getElementById("f-trainer-device").value = "cpu";

document.getElementById("f-adv-nnodes").value = "2";
document.getElementById("f-adv-gpn").value = "4";
document.getElementById("f-command").value = `PYTHONUNBUFFERED=1 \\\\\\npython3 -m verl.trainer.main_ppo \\\\\\n  data.train_files=$HOME/common/datasets/gsm8k/train.parquet \\\\\\n  data.val_files=$HOME/common/datasets/gsm8k/test.parquet \\\\\\n  +ray_kwargs.ray_init.address=auto`;

bindStepper("f-nnodes", "f-nnodes-dec", "f-nnodes-inc", 1);
bindStepper("f-gpn", "f-gpn-dec", "f-gpn-inc", 0);
bindStepper("f-epochs", "f-epochs-dec", "f-epochs-inc", 1);
bindStepper("f-adv-nnodes", "f-adv-nnodes-dec", "f-adv-nnodes-inc", 1);
bindStepper("f-adv-gpn", "f-adv-gpn-dec", "f-adv-gpn-inc", 0);

document.getElementById("f-tpl").onchange = () => {
  const tpl = document.getElementById("f-tpl").value;
  if (tpl === "ppo") {
    document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet";
    document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet";
  } else if (tpl === "grpo") {
    document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet";
    document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet";
  } else if (tpl === "sft") {
    document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k_sft/train.parquet";
    document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k_sft/test.parquet";
  } else if (tpl === "advanced") {
    document.getElementById("f-adv-nnodes").value = "2";
    document.getElementById("f-adv-gpn").value = "4";
    document.getElementById("f-command").value = TPL_ADV.split("command: |")[1]?.trimStart() || document.getElementById("f-command").value;
  } else if (tpl === "merge") {
    document.getElementById("f-adv-nnodes").value = "1";
    document.getElementById("f-adv-gpn").value = "0";
    document.getElementById("f-command").value = TPL_MERGE.split("command: |")[1]?.trimStart() || document.getElementById("f-command").value;
  }
  updateFormVisibility();
  updatePreview();
};

for (const id of ["f-code-path","f-model-id","f-train-file","f-val-file","f-nnodes","f-gpn","f-epochs","f-steps","f-save-freq","f-test-freq","f-trainer-device","f-adv-nnodes","f-adv-gpn","f-command"]) {
  const el = document.getElementById(id);
  if (el) el.oninput = () => updatePreview();
}

document.getElementById("mode-yaml").onclick = () => {
  // When switching to YAML, sync from form if form is visible.
  const formVisible = document.getElementById("panel-form").style.display !== "none";
  if (formVisible) yamlEl.value = updatePreview();
  showMode("yaml");
};
document.getElementById("mode-form").onclick = () => {
  showMode("form");
};

showMode("yaml");

document.getElementById("submit").onclick = async () => {
  msg.textContent = "Submitting...";
  const body = (document.getElementById("panel-form").style.display !== "none") ? updatePreview() : yamlEl.value;
  const resp = await apiFetch("/api/v2/tasks", { method: "POST", headers: {"Content-Type":"text/plain"}, body });
  const text = await resp.text();
  if (!resp.ok) { msg.textContent = "Error: " + resp.status + "\\n" + text; return; }
  const obj = JSON.parse(text);
  msg.textContent = "OK: " + fmtJson(obj);
  if (obj.task_id) window.location.href = "/ui/tasks/" + obj.task_id;
};
		""".strip()
            .replace("__TPL_PPO__", tpl_ppo)
            .replace("__TPL_GRPO__", tpl_grpo)
            .replace("__TPL_SFT__", tpl_sft)
            .replace("__TPL_ADV__", tpl_adv)
            .replace("__TPL_EVAL__", tpl_eval)
            .replace("__TPL_MERGE__", tpl_merge)
        )
        return HTMLResponse(content=page("New Task", "new", body, script))

