187 lines
7.5 KiB
JavaScript
187 lines
7.5 KiB
JavaScript
// 新建任务弹框:任务名 / 工作目录(新建 sentinel 或复用已有,二级 input 联动)/
|
|
// 描述 / 智能体(skill)/ 模型 select,提交 POST /v1/tasks。
|
|
// 顶层自绑 hd-new 打开、nt-go 提交、各 input 联动;唯一对外导出 loadFolderSuggestions
|
|
//(供 main enterApp 初始化顶部 filter-wd、files 复制/移动后刷新目录列表)。
|
|
import { $ } from "./dom.js";
|
|
import { state } from "./state.js";
|
|
import { api } from "./api.js";
|
|
import { escapeHtml } from "./format.js";
|
|
import { logout } from "./auth.js";
|
|
import { loadModels, loadTaskList, selectTask } from "./chat.js";
|
|
|
|
// ───── new task ─────
|
|
// wd 二级 input 默认跟随 name;用户一旦手改二级 input → 脱钩;清空二级 input 重置 flag
|
|
let wdManuallyEdited = false;
|
|
$("hd-new").onclick = async () => {
|
|
$("nt-name").value = "";
|
|
$("nt-wd-sel").value = "__new__"; // 默认选 sentinel
|
|
$("nt-wd-new").value = "";
|
|
$("nt-wd-new").style.display = ""; // sentinel 选中态 → 二级 input 可见
|
|
$("nt-desc").value = ""; $("nt-skill").value = "";
|
|
$("nt-err").textContent = "";
|
|
$("nt-wd-hint").textContent = "";
|
|
wdManuallyEdited = false;
|
|
$("new-task-modal").classList.add("show");
|
|
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
|
|
$("nt-wd-sel").value = "__new__"; // populateFolderSelects 重渲后再保险一次
|
|
populateModelSelect();
|
|
$("nt-name").focus();
|
|
};
|
|
function populateModelSelect() {
|
|
const sel = $("nt-model");
|
|
const models = state.models || [];
|
|
if (models.length === 0) {
|
|
sel.innerHTML = `<option value="">(默认)</option>`;
|
|
return;
|
|
}
|
|
sel.innerHTML = models.map(m =>
|
|
`<option value="${escapeHtml(m.profile)}" ${m.is_default ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
|
|
).join("");
|
|
}
|
|
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
|
$("nt-go").onclick = async () => {
|
|
const name = $("nt-name").value.trim();
|
|
const sel = $("nt-wd-sel").value;
|
|
// sentinel:用二级 input 值,空则 fallback name;选已有目录:直接用 value
|
|
const working_dir = sel === "__new__"
|
|
? ($("nt-wd-new").value.trim() || name)
|
|
: sel;
|
|
const desc = $("nt-desc").value.trim();
|
|
const skill = $("nt-skill").value;
|
|
const model_profile = $("nt-model").value;
|
|
$("nt-err").textContent = "";
|
|
if (!name) { $("nt-err").textContent = "任务名为必填项"; return; }
|
|
try {
|
|
const t = await api("POST", "/v1/tasks",
|
|
{ name, working_dir, description: desc, skill, model_profile });
|
|
$("new-task-modal").classList.remove("show");
|
|
await loadTaskList();
|
|
selectTask(t.task_id);
|
|
} catch (e) {
|
|
if (e.status === 401) { logout(); return; }
|
|
$("nt-err").textContent = e.message;
|
|
}
|
|
};
|
|
|
|
// 工作目录:拉数据 + 灌两个 select(顶部 filter-wd 和 modal nt-wd-sel)
|
|
export async function loadFolderSuggestions() {
|
|
try {
|
|
const data = await api("GET", "/v1/folders");
|
|
state.folders = data.folders || [];
|
|
} catch (e) {
|
|
state.folders = state.folders || [];
|
|
}
|
|
populateFolderSelects();
|
|
}
|
|
|
|
// 灌 filter-wd + nt-wd-sel options;保留当前选中值
|
|
function populateFolderSelects() {
|
|
const folders = state.folders || [];
|
|
// 顶部 filter:第一项 "(全部目录)" sentinel
|
|
const filterSel = $("filter-wd");
|
|
const filterCur = filterSel.value;
|
|
const filterOpts = ['<option value="">(全部目录)</option>'];
|
|
for (const f of folders) {
|
|
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
|
filterOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
|
}
|
|
filterSel.innerHTML = filterOpts.join("");
|
|
filterSel.value = filterCur; // 重渲后恢复选中
|
|
// modal wd:第一项 "+ 新建(跟随任务名)" sentinel(label 由 updateSentinelLabel 实时刷)
|
|
const wdSel = $("nt-wd-sel");
|
|
const wdCur = wdSel.value || "__new__";
|
|
const wdOpts = [`<option value="__new__">+ 新建(跟随任务名)</option>`];
|
|
for (const f of folders) {
|
|
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
|
wdOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
|
}
|
|
wdSel.innerHTML = wdOpts.join("");
|
|
wdSel.value = wdCur;
|
|
updateSentinelLabel(); // 用最新的 name 刷 sentinel
|
|
}
|
|
|
|
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
|
|
async function loadSkillOptions() {
|
|
const sel = $("nt-skill");
|
|
if (!state.skills) {
|
|
try {
|
|
const data = await api("GET", "/v1/skills");
|
|
state.skills = data.skills || [];
|
|
} catch (e) {
|
|
state.skills = []; // 静默兜底,select 仍保留"(默认)"项
|
|
}
|
|
}
|
|
// 渲染:第一项固定为"默认"(空 value),其后逐 skill 一项
|
|
const opts = ['<option value="">(默认 · 不限定)</option>'];
|
|
for (const s of state.skills) {
|
|
const label = `${s.name}${s.description ? " — " + s.description : ""}`;
|
|
opts.push(`<option value="${escapeHtml(s.name)}" title="${escapeHtml(s.description || "")}">${escapeHtml(label)}</option>`);
|
|
}
|
|
sel.innerHTML = opts.join("");
|
|
sel.value = ""; // hd-new 已清空,这里幂等再保一次
|
|
}
|
|
|
|
// === modal wd select + 二级 input 联动 ===
|
|
// select 选 "__new__" sentinel → 显示二级 input(默认值跟随 name);选已有目录 → 隐藏二级 input
|
|
// wdManuallyEdited:用户改过二级 input 后置 true,name 不再覆盖;清空二级 input 重置 false
|
|
|
|
function updateSentinelLabel() {
|
|
// sentinel 永远是 select 第一项,labels 实时含 name 让用户一眼知会建什么
|
|
const sel = $("nt-wd-sel");
|
|
const opt = sel.options[0];
|
|
if (!opt || opt.value !== "__new__") return;
|
|
const name = $("nt-name").value.trim();
|
|
opt.textContent = name ? `+ 新建「${name}」` : `+ 新建(跟随任务名)`;
|
|
}
|
|
|
|
function updateWdHint() {
|
|
const hint = $("nt-wd-hint");
|
|
const sel = $("nt-wd-sel").value;
|
|
if (sel === "__new__") {
|
|
const v = $("nt-wd-new").value.trim();
|
|
const name = $("nt-name").value.trim();
|
|
const target = v || name;
|
|
if (!target) { hint.textContent = ""; return; }
|
|
// 用户手输的新名恰好命中已有目录 → 提示会复用而非新建
|
|
const collision = (state.folders || []).find(f => f.name === target);
|
|
if (collision) {
|
|
const n = collision.n_tasks || 0;
|
|
hint.innerHTML = `<span style="color:var(--accent);">! 已有同名目录,将复用</span> · ${n} 个任务`;
|
|
} else {
|
|
hint.innerHTML = `<span class="muted">→ 新建目录 ${escapeHtml(target)}</span>`;
|
|
}
|
|
} else {
|
|
const f = (state.folders || []).find(x => x.name === sel);
|
|
const n = f ? (f.n_tasks || 0) : 0;
|
|
hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`;
|
|
}
|
|
}
|
|
|
|
// name 改变 → 更新 sentinel label;若未脱钩且当前是 sentinel,二级 input 跟随 name
|
|
$("nt-name").addEventListener("input", () => {
|
|
updateSentinelLabel();
|
|
if (!wdManuallyEdited && $("nt-wd-sel").value === "__new__") {
|
|
$("nt-wd-new").value = $("nt-name").value;
|
|
}
|
|
updateWdHint();
|
|
});
|
|
|
|
// wd select 切换 → 切显示二级 input + 刷 hint
|
|
$("nt-wd-sel").addEventListener("change", () => {
|
|
const v = $("nt-wd-sel").value;
|
|
if (v === "__new__") {
|
|
$("nt-wd-new").style.display = "";
|
|
if (!wdManuallyEdited) $("nt-wd-new").value = $("nt-name").value;
|
|
} else {
|
|
$("nt-wd-new").style.display = "none";
|
|
}
|
|
updateWdHint();
|
|
});
|
|
|
|
// 二级 input 改变 → 非空视为手动修改;清空重置 flag 但保持空(避免 backspace 想换名时被打断)
|
|
$("nt-wd-new").addEventListener("input", () => {
|
|
wdManuallyEdited = $("nt-wd-new").value.trim() !== "";
|
|
updateWdHint();
|
|
});
|
|
|