// 新建任务弹框:可选任务名 / 可搜索工作目录组合框(复用“开始一段新对话”交互)/ // 描述 / 智能体(skill)/ 模型 select,提交 POST /v1/tasks。 // 顶层自绑 hd-new-custom 打开、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 ───── const LS_RECENT_WORKING_DIR = "zcbot.recent-working-dir"; let customWorkingDir = ""; let customDirNewMode = false; let customDirActiveIndex = -1; $("hd-new-custom").onclick = async () => { $("nt-name").value = ""; customWorkingDir = ""; customDirNewMode = false; $("nt-wd-picker").value = ""; $("nt-wd-picker").dataset.searching = ""; setCustomFolderOpen(false); $("nt-desc").value = ""; $("nt-skill").value = ""; $("nt-err").textContent = ""; $("new-task-modal").classList.add("show"); await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]); renderCustomFolderOptions(); syncCustomDirHint(); populateModelSelect(); $("nt-name").focus(); }; function populateModelSelect() { const sel = $("nt-model"); const models = state.models || []; if (models.length === 0) { sel.innerHTML = ``; return; } sel.innerHTML = models.map(m => `` ).join(""); } $("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show"); $("nt-go").onclick = async () => { const name = $("nt-name").value.trim(); const working_dir = currentCustomWorkingDir(); const desc = $("nt-desc").value.trim(); const skill = $("nt-skill").value; const model_profile = $("nt-model").value; $("nt-err").textContent = ""; if (!working_dir) { $("nt-err").textContent = "请选择已有工作目录或创建新目录"; return; } try { const body = { working_dir, description: desc, skill, model_profile }; if (name) body.name = name; const t = await api("POST", "/v1/tasks", body); localStorage.setItem(LS_RECENT_WORKING_DIR, working_dir); $("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; } }; // 工作目录:拉数据 + 灌顶部 filter-wd,并刷新自定义创建组合框。 export async function loadFolderSuggestions() { try { const data = await api("GET", "/v1/folders"); state.folders = data.folders || []; } catch (e) { state.folders = state.folders || []; } populateFolderSelects(); renderCustomFolderOptions(); } // 灌顶部 filter-wd;自定义创建弹框使用与快速新对话一致的组合框。 function populateFolderSelects() { const folders = state.folders || []; // 顶部 filter:第一项 "(全部目录)" sentinel const filterSel = $("filter-wd"); const filterCur = filterSel.value; const filterOpts = ['']; for (const f of folders) { const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`; filterOpts.push(``); } filterSel.innerHTML = filterOpts.join(""); filterSel.value = filterCur; // 重渲后恢复选中 } // 智能体类型下拉: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 = ['']; for (const s of state.skills) { const label = `${s.name}${s.description ? " — " + s.description : ""}`; opts.push(``); } sel.innerHTML = opts.join(""); sel.value = ""; // hd-new 已清空,这里幂等再保一次 } // === 自定义创建目录组合框(与“开始一段新对话”一致)=== // UI 新契约:工作目录始终必填,任务名选填;不再用任务名自动补目录。 function currentCustomWorkingDir() { return customWorkingDir.trim(); } function customDirIsNew() { return !!currentCustomWorkingDir() && customDirNewMode; } function syncCustomDirHint() { const target = currentCustomWorkingDir(); $("nt-wd-hint").textContent = target ? customDirIsNew() ? `将新建工作目录「${target}」` : `文件将保存到「${target}」` : "可选择已有目录,也可以新建目录。"; } function renderCustomFolderOptions() { const input = $("nt-wd-picker"); const list = $("nt-wd-options"); if (!input || !list) return; const query = input.dataset.searching === "1" ? input.value.trim().toLocaleLowerCase() : ""; const recent = localStorage.getItem(LS_RECENT_WORKING_DIR) || ""; const folders = [...(state.folders || [])]; folders.sort((a, b) => Number(b.name === recent) - Number(a.name === recent)); const visible = query ? folders.filter(f => f.name.toLocaleLowerCase().includes(query)) : folders; const exact = query ? folders.some(f => f.name.toLocaleLowerCase() === query) : false; const selected = customDirIsNew() ? "" : currentCustomWorkingDir(); const existingHtml = visible.map((f, i) => { const tag = f.n_tasks ? `${f.n_tasks} 个任务` : "空目录"; const recentTag = f.name === recent ? `最近使用` : ""; const chosen = f.name === selected ? " selected" : ""; return ``; }).join(""); const createHtml = query && !exact ? `` : `
输入新名称可创建工作目录
`; const emptyHtml = !visible.length && !query ? `
暂无已有目录
` : ""; list.innerHTML = existingHtml + emptyHtml + createHtml; if (input.dataset.searching !== "1") input.value = currentCustomWorkingDir(); customDirActiveIndex = -1; input.setAttribute("aria-activedescendant", ""); list.querySelectorAll(".new-chat-dir-option").forEach(option => { option.onmousedown = e => e.preventDefault(); option.onclick = () => selectCustomFolder( option.dataset.create || option.dataset.name || "", !!option.dataset.create, ); }); } function setCustomFolderOpen(open) { const combo = $("nt-wd-combobox"); const input = $("nt-wd-picker"); combo.classList.toggle("open", open); input.setAttribute("aria-expanded", String(open)); if (!open) { input.dataset.searching = ""; input.value = currentCustomWorkingDir(); customDirActiveIndex = -1; input.setAttribute("aria-activedescendant", ""); } } function selectCustomFolder(name, isNew) { const trimmed = name.trim(); if (!trimmed) return; customWorkingDir = trimmed; customDirNewMode = isNew; setCustomFolderOpen(false); syncCustomDirHint(); renderCustomFolderOptions(); } function moveCustomFolderActive(delta) { const input = $("nt-wd-picker"); const options = [...document.querySelectorAll("#nt-wd-options .new-chat-dir-option")]; if (!options.length) return; customDirActiveIndex = ( customDirActiveIndex + delta + options.length ) % options.length; options.forEach((option, i) => option.classList.toggle("active", i === customDirActiveIndex)); const active = options[customDirActiveIndex]; input.setAttribute("aria-activedescendant", active.id); active.scrollIntoView({ block: "nearest" }); } const customPicker = $("nt-wd-picker"); const customCombo = $("nt-wd-combobox"); customPicker.onfocus = () => { customPicker.dataset.searching = ""; setCustomFolderOpen(true); renderCustomFolderOptions(); customPicker.select(); }; customPicker.oninput = () => { customPicker.dataset.searching = "1"; setCustomFolderOpen(true); renderCustomFolderOptions(); }; customPicker.onkeydown = e => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setCustomFolderOpen(true); moveCustomFolderActive(e.key === "ArrowDown" ? 1 : -1); } else if (e.key === "Enter" && customCombo.classList.contains("open")) { const options = [...document.querySelectorAll("#nt-wd-options .new-chat-dir-option")]; const active = options[customDirActiveIndex]; const typed = customPicker.value.trim().toLocaleLowerCase(); const exact = options.find( o => (o.dataset.name || "").toLocaleLowerCase() === typed, ); const target = active || exact || (options.length === 1 ? options[0] : null); if (target) { e.preventDefault(); selectCustomFolder( target.dataset.create || target.dataset.name || "", !!target.dataset.create, ); } } else if (e.key === "Escape") { e.preventDefault(); setCustomFolderOpen(false); customPicker.blur(); } }; customPicker.onblur = () => setTimeout(() => setCustomFolderOpen(false), 0); $("nt-wd-toggle").onmousedown = e => e.preventDefault(); $("nt-wd-toggle").onclick = () => { const open = !customCombo.classList.contains("open"); setCustomFolderOpen(open); if (open) { customPicker.focus(); customPicker.select(); renderCustomFolderOptions(); } };