ui: 工作目录回到原生 select + sentinel + 二级 input (modal + 顶部 filter)
combobox 方案推翻 — 即使 show 不过滤,modal wd 因联动有值后用户直觉仍是 "得点开下拉看选项",自实现 panel 不如浏览器原生 select 稳。 - modal nt-wd-sel 第一项 sentinel "+ 新建「<name>」"(updateSentinelLabel 跟 name 实时刷),sentinel 选中显示二级 nt-wd-new 默认跟随 name, 选已有目录隐藏;wdManuallyEdited 锚到二级 input - 顶部 filter-wd 改 select,onchange → loadTaskList(无 debounce) - loadFolderSuggestions + populateFolderSelects 灌两个 select,保留当前选中 - enterApp fire-and-forget 预拉 folders 让左 pane 一打开就有选项 - hint 在"新名碰到同名"时提示"将复用而非新建" - combobox 工厂 + .combo CSS + datalist 残留全删 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32a8c348a8
commit
fa6cb72103
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
### 2026-05-21
|
### 2026-05-21
|
||||||
|
|
||||||
- **新建任务弹窗 combobox 从 datalist 改自定义 dropdown(modal + 顶部 filter-wd 双处)**:接上一条迭代 —— `<input list="folders-datalist">` 在 wd 因联动有了值之后,浏览器按前缀过滤把下拉过滤没了,用户只能点右侧三角看完整列表,体验比原 `<select>` 还差。改方案:抽 `makeFolderCombo({input, panel, onPick})` 工厂函数 —— input 旁加 absolute 定位的 `.combo-panel`,focus/click 弹出完整列表、input 时子串过滤(不再前缀)、点击项填入(mousedown preventDefault 阻止 input blur 提前关 panel)、blur 延迟 120ms 兜底关闭、键盘 ↑↓ Enter Esc。`.combo` / `.combo-panel` 样式提到全局。modal `nt-wd-sel` 和顶部 `filter-wd` 都走该工厂,各自传 `onPick`(modal 置 `wdManuallyEdited` + `updateWdHint`,filter 走 `loadTaskList`)。datalist 元素 + `loadFolderSuggestions` 灌它的逻辑全删,`ensureFoldersLoaded` 改用 `state.folders` 是否定义判断。
|
- **工作目录回到原生 `<select>` + sentinel + 二级 input(modal + 顶部 filter)**:combobox 方案推翻 —— 即使 show 时不过滤,modal 里 wd 因联动有值之后用户的直觉仍然是"我得点开下拉看选项",自己实现的 panel 总不如浏览器原生 select 稳。改回 select 范式:① modal `nt-wd-sel` 第一项 sentinel `+ 新建「<name>」`(label 由 `updateSentinelLabel` 跟 name 实时刷)+ 其后已有目录列表;sentinel 选中时显示二级 `nt-wd-new` 输入框默认值跟随 name,选已有目录时隐藏。`wdManuallyEdited` 锚到二级 input 上(用户改它就脱钩,清空恢复跟随)。② 顶部 `filter-wd` 也改成 `<select>`,首项 `(全部目录)`,onchange → `loadTaskList`;原 input 的 debounce listener 删,搜索 `filter-q` 的 debounce 保留独立写。③ `loadFolderSuggestions` 拉数据 + 新增 `populateFolderSelects` 灌两个 select(保留当前选中值);`enterApp` 启动时 fire-and-forget 预拉一次让左 pane 一打开就有选项。④ hint 在"输入新名恰好命中已有"时提示"将复用而非新建"。combobox 工厂 + .combo CSS + datalist 残留全删。
|
||||||
- **新建任务弹窗工作目录改 combobox + name 联动**:`web/static/dev.html` modal 里 `nt-wd-sel` 从 `<select>` 改成 `<input list="folders-datalist">`,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)` → `(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。
|
- **新建任务弹窗工作目录改 combobox + name 联动**:`web/static/dev.html` modal 里 `nt-wd-sel` 从 `<select>` 改成 `<input list="folders-datalist">`,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)` → `(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。
|
||||||
- **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。
|
- **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。
|
||||||
- **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。
|
- **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。
|
||||||
|
|
|
||||||
|
|
@ -525,24 +525,6 @@
|
||||||
#new-task-modal label { display: block; margin-top: 8px; font-size: 12px; color: var(--muted); }
|
#new-task-modal label { display: block; margin-top: 8px; font-size: 12px; color: var(--muted); }
|
||||||
#new-task-modal .err { color: var(--accent); font-size: 12px; margin-top: 8px; min-height: 1em; }
|
#new-task-modal .err { color: var(--accent); font-size: 12px; margin-top: 8px; min-height: 1em; }
|
||||||
#new-task-modal .actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; }
|
#new-task-modal .actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
/* 自定义 combobox(替代原生 datalist —— datalist 在 input 非空时下拉被前缀过滤丢失) */
|
|
||||||
.combo { position: relative; }
|
|
||||||
.combo-panel {
|
|
||||||
position: absolute; top: calc(100% + 2px); left: 0; right: 0;
|
|
||||||
background: #fff; border: 1px solid var(--border); border-radius: 6px;
|
|
||||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
|
||||||
max-height: 220px; overflow-y: auto; z-index: 10;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.combo-panel.show { display: block; }
|
|
||||||
.combo-panel .item {
|
|
||||||
display: flex; justify-content: space-between; align-items: baseline;
|
|
||||||
padding: 6px 10px; cursor: pointer; gap: 12px; font-size: 13px;
|
|
||||||
}
|
|
||||||
.combo-panel .item:hover,
|
|
||||||
.combo-panel .item.active { background: var(--hover); }
|
|
||||||
.combo-panel .item .tag { color: var(--muted); font-size: 11px; flex-shrink: 0; }
|
|
||||||
.combo-panel .empty { padding: 8px 10px; color: var(--muted); font-size: 12px; text-align: center; }
|
|
||||||
|
|
||||||
/* ───── file preview modal ───── */
|
/* ───── file preview modal ───── */
|
||||||
#file-preview-modal {
|
#file-preview-modal {
|
||||||
|
|
@ -714,10 +696,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-head" style="gap: 6px;">
|
<div class="pane-head" style="gap: 6px;">
|
||||||
<input id="filter-q" class="small" placeholder="搜索名称/描述..." style="flex:1; padding: 3px 6px;" />
|
<input id="filter-q" class="small" placeholder="搜索名称/描述..." style="flex:1; padding: 3px 6px;" />
|
||||||
<div class="combo" style="flex:1;">
|
<select id="filter-wd" class="small" style="flex:1; padding: 3px 6px;">
|
||||||
<input id="filter-wd" class="small" placeholder="工作目录" autocomplete="off" style="width:100%; padding: 3px 6px;" />
|
<option value="">(全部目录)</option>
|
||||||
<div class="combo-panel" id="filter-wd-panel"></div>
|
</select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-head" style="gap: 6px;">
|
<div class="pane-head" style="gap: 6px;">
|
||||||
<span class="small muted" style="white-space:nowrap;">排序</span>
|
<span class="small muted" style="white-space:nowrap;">排序</span>
|
||||||
|
|
@ -805,11 +786,11 @@
|
||||||
<h3>新建任务</h3>
|
<h3>新建任务</h3>
|
||||||
<label for="nt-name">任务名(必填)</label>
|
<label for="nt-name">任务名(必填)</label>
|
||||||
<input id="nt-name" placeholder="例如 初稿大纲" />
|
<input id="nt-name" placeholder="例如 初稿大纲" />
|
||||||
<label for="nt-wd-sel">工作目录(默认跟随任务名;可输入新名或选已有目录复用)</label>
|
<label for="nt-wd-sel">工作目录</label>
|
||||||
<div class="combo">
|
<select id="nt-wd-sel">
|
||||||
<input id="nt-wd-sel" placeholder="输入或选已有目录(↑↓ Enter Esc)" autocomplete="off" />
|
<option value="__new__">+ 新建(跟随任务名)</option>
|
||||||
<div class="combo-panel" id="nt-wd-panel"></div>
|
</select>
|
||||||
</div>
|
<input id="nt-wd-new" placeholder="新目录名" style="margin-top:6px;" />
|
||||||
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
|
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
|
||||||
<label for="nt-desc">描述(可选,任务长描述)</label>
|
<label for="nt-desc">描述(可选,任务长描述)</label>
|
||||||
<input id="nt-desc" />
|
<input id="nt-desc" />
|
||||||
|
|
@ -1202,6 +1183,7 @@ function enterApp() {
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
loadFiles(); // 文件面板与 task 解耦 — 启动即拉 user_root
|
loadFiles(); // 文件面板与 task 解耦 — 启动即拉 user_root
|
||||||
loadModels(); // 模型清单缓存:chat-meta 下拉 + 新建对话框 + 历史小标
|
loadModels(); // 模型清单缓存:chat-meta 下拉 + 新建对话框 + 历史小标
|
||||||
|
loadFolderSuggestions(); // 灌 filter-wd select(modal 打开时会重拉,这里让左 pane 先有选项)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadModels() {
|
async function loadModels() {
|
||||||
|
|
@ -1363,16 +1345,15 @@ function taskMenuItems(t) {
|
||||||
// 筛选 / 排序 / 刷新 一律 reset(loadTaskList 默认 append=false);追加由 sentinel observer 触发
|
// 筛选 / 排序 / 刷新 一律 reset(loadTaskList 默认 append=false);追加由 sentinel observer 触发
|
||||||
$("filter-status").onchange = () => loadTaskList();
|
$("filter-status").onchange = () => loadTaskList();
|
||||||
$("filter-order").onchange = () => loadTaskList();
|
$("filter-order").onchange = () => loadTaskList();
|
||||||
|
$("filter-wd").onchange = () => loadTaskList(); // select 选完立即筛
|
||||||
$("btn-refresh-tasks").onclick = () => loadTaskList();
|
$("btn-refresh-tasks").onclick = () => loadTaskList();
|
||||||
|
|
||||||
// 搜索 / 工作目录筛选:debounce 300ms,避免每个字符都打 API
|
// 搜索 q 是 text input → 300ms debounce 避免每字符打 API
|
||||||
let _filterDebounce = null;
|
let _filterDebounce = null;
|
||||||
function scheduleFilter() {
|
$("filter-q").addEventListener("input", () => {
|
||||||
clearTimeout(_filterDebounce);
|
clearTimeout(_filterDebounce);
|
||||||
_filterDebounce = setTimeout(() => loadTaskList(), 300);
|
_filterDebounce = setTimeout(() => loadTaskList(), 300);
|
||||||
}
|
});
|
||||||
$("filter-q").addEventListener("input", scheduleFilter);
|
|
||||||
$("filter-wd").addEventListener("input", scheduleFilter);
|
|
||||||
|
|
||||||
// 滚动加载:左 pane 整体是 scroll 容器(.pane{overflow:auto}),用 #pane-left 作 root
|
// 滚动加载:左 pane 整体是 scroll 容器(.pane{overflow:auto}),用 #pane-left 作 root
|
||||||
// rootMargin 提前 200px 触发,体感更顺;阈值 0 即可(刚进入即触发,append 期间 taskLoading 自带防抖)
|
// rootMargin 提前 200px 触发,体感更顺;阈值 0 即可(刚进入即触发,append 期间 taskLoading 自带防抖)
|
||||||
|
|
@ -1382,11 +1363,6 @@ const _taskScrollObserver = new IntersectionObserver((entries) => {
|
||||||
}
|
}
|
||||||
}, { root: $("pane-left"), rootMargin: "200px 0px" });
|
}, { root: $("pane-left"), rootMargin: "200px 0px" });
|
||||||
_taskScrollObserver.observe($("task-sentinel"));
|
_taskScrollObserver.observe($("task-sentinel"));
|
||||||
// 顶部 filter-wd combobox 首次 focus 拉一次 folders;modal 打开会重拉(数据可能变)
|
|
||||||
async function ensureFoldersLoaded() {
|
|
||||||
if (!state.folders) await loadFolderSuggestions();
|
|
||||||
}
|
|
||||||
$("filter-wd").addEventListener("focus", ensureFoldersLoaded);
|
|
||||||
|
|
||||||
// ───── select task ─────
|
// ───── select task ─────
|
||||||
async function selectTask(tid) {
|
async function selectTask(tid) {
|
||||||
|
|
@ -2839,18 +2815,20 @@ async function uploadSelected() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───── new task ─────
|
// ───── new task ─────
|
||||||
// wd 跟随 name 自动同步;用户一旦手动改 wd → 脱钩;若再清空 wd → 恢复跟随
|
// wd 二级 input 默认跟随 name;用户一旦手改二级 input → 脱钩;清空二级 input 重置 flag
|
||||||
let wdManuallyEdited = false;
|
let wdManuallyEdited = false;
|
||||||
$("hd-new").onclick = async () => {
|
$("hd-new").onclick = async () => {
|
||||||
$("nt-name").value = "";
|
$("nt-name").value = "";
|
||||||
$("nt-wd-sel").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-desc").value = ""; $("nt-skill").value = "";
|
||||||
$("nt-err").textContent = "";
|
$("nt-err").textContent = "";
|
||||||
$("nt-wd-hint").textContent = "";
|
$("nt-wd-hint").textContent = "";
|
||||||
wdManuallyEdited = false;
|
wdManuallyEdited = false;
|
||||||
wdCombo.hide();
|
|
||||||
$("new-task-modal").classList.add("show");
|
$("new-task-modal").classList.add("show");
|
||||||
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
|
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
|
||||||
|
$("nt-wd-sel").value = "__new__"; // populateFolderSelects 重渲后再保险一次
|
||||||
populateModelSelect();
|
populateModelSelect();
|
||||||
$("nt-name").focus();
|
$("nt-name").focus();
|
||||||
};
|
};
|
||||||
|
|
@ -2865,11 +2843,14 @@ function populateModelSelect() {
|
||||||
`<option value="${escapeHtml(m.profile)}" ${m.is_default ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
|
`<option value="${escapeHtml(m.profile)}" ${m.is_default ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
|
||||||
).join("");
|
).join("");
|
||||||
}
|
}
|
||||||
$("nt-cancel").onclick = () => { wdCombo.hide(); $("new-task-modal").classList.remove("show"); };
|
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
||||||
$("nt-go").onclick = async () => {
|
$("nt-go").onclick = async () => {
|
||||||
const name = $("nt-name").value.trim();
|
const name = $("nt-name").value.trim();
|
||||||
// wd 走联动 + fallback:正常情况已跟随 name;若用户清空了 wd 也兜底用 name
|
const sel = $("nt-wd-sel").value;
|
||||||
const working_dir = $("nt-wd-sel").value.trim() || name;
|
// sentinel:用二级 input 值,空则 fallback name;选已有目录:直接用 value
|
||||||
|
const working_dir = sel === "__new__"
|
||||||
|
? ($("nt-wd-new").value.trim() || name)
|
||||||
|
: sel;
|
||||||
const desc = $("nt-desc").value.trim();
|
const desc = $("nt-desc").value.trim();
|
||||||
const skill = $("nt-skill").value;
|
const skill = $("nt-skill").value;
|
||||||
const model_profile = $("nt-model").value;
|
const model_profile = $("nt-model").value;
|
||||||
|
|
@ -2878,7 +2859,6 @@ $("nt-go").onclick = async () => {
|
||||||
try {
|
try {
|
||||||
const t = await api("POST", "/v1/tasks",
|
const t = await api("POST", "/v1/tasks",
|
||||||
{ name, working_dir, description: desc, skill, model_profile });
|
{ name, working_dir, description: desc, skill, model_profile });
|
||||||
wdCombo.hide();
|
|
||||||
$("new-task-modal").classList.remove("show");
|
$("new-task-modal").classList.remove("show");
|
||||||
await loadTaskList();
|
await loadTaskList();
|
||||||
selectTask(t.task_id);
|
selectTask(t.task_id);
|
||||||
|
|
@ -2888,14 +2868,41 @@ $("nt-go").onclick = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 工作目录:供 combobox panel 和 hint 共用,缓存到 state.folders
|
// 工作目录:拉数据 + 灌两个 select(顶部 filter-wd 和 modal nt-wd-sel)
|
||||||
async function loadFolderSuggestions() {
|
async function loadFolderSuggestions() {
|
||||||
try {
|
try {
|
||||||
const data = await api("GET", "/v1/folders");
|
const data = await api("GET", "/v1/folders");
|
||||||
state.folders = data.folders || [];
|
state.folders = data.folders || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state.folders = state.folders || []; // 失败保留旧值;首次失败也置空数组,combobox 显示"无已有目录"
|
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
|
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
|
||||||
|
|
@ -2919,111 +2926,66 @@ async function loadSkillOptions() {
|
||||||
sel.value = ""; // hd-new 已清空,这里幂等再保一次
|
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() {
|
function updateWdHint() {
|
||||||
const v = $("nt-wd-sel").value.trim();
|
|
||||||
const hint = $("nt-wd-hint");
|
const hint = $("nt-wd-hint");
|
||||||
if (!v) {
|
const sel = $("nt-wd-sel").value;
|
||||||
// wd 空 → 提交时会 fallback 到 name(见 nt-go);若 name 也空则不显示
|
if (sel === "__new__") {
|
||||||
const fallback = $("nt-name").value.trim();
|
const v = $("nt-wd-new").value.trim();
|
||||||
hint.textContent = fallback ? `→ 用任务名「${fallback}」作目录` : "";
|
const name = $("nt-name").value.trim();
|
||||||
return;
|
const target = v || name;
|
||||||
}
|
if (!target) { hint.textContent = ""; return; }
|
||||||
const hit = (state.folders || []).find(f => f.name === v);
|
// 用户手输的新名恰好命中已有目录 → 提示会复用而非新建
|
||||||
if (hit) {
|
const collision = (state.folders || []).find(f => f.name === target);
|
||||||
const n = hit.n_tasks || 0;
|
if (collision) {
|
||||||
hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`;
|
const n = collision.n_tasks || 0;
|
||||||
|
hint.innerHTML = `<span style="color:var(--accent);">! 已有同名目录,将复用</span> · ${n} 个任务`;
|
||||||
} else {
|
} else {
|
||||||
hint.innerHTML = `<span class="muted">→ 新建目录 ${escapeHtml(v)}</span>`;
|
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} 个任务`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === folder combobox 工厂(自定义,代替 datalist —— datalist 在 input 非空时下拉被前缀过滤丢失) ===
|
// name 改变 → 更新 sentinel label;若未脱钩且当前是 sentinel,二级 input 跟随 name
|
||||||
// 数据源统一走 state.folders(loadFolderSuggestions 维护);onPick(name) 是选中后的回调
|
|
||||||
function makeFolderCombo({ input, panel, onPick }) {
|
|
||||||
let filtered = [];
|
|
||||||
let activeIdx = -1;
|
|
||||||
function render() {
|
|
||||||
const all = state.folders || [];
|
|
||||||
const f = (input.value || "").trim().toLowerCase();
|
|
||||||
filtered = all.filter(x => !f || x.name.toLowerCase().includes(f));
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
panel.innerHTML = `<div class="empty">${all.length === 0 ? "无已有目录" : "无匹配 · 将新建"}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panel.innerHTML = filtered.map((x, i) => {
|
|
||||||
const tag = x.n_tasks ? `${x.n_tasks} 个任务` : `空目录`;
|
|
||||||
return `<div class="item${i === activeIdx ? " active" : ""}" data-idx="${i}">`
|
|
||||||
+ `<span class="name">${escapeHtml(x.name)}</span>`
|
|
||||||
+ `<span class="tag">${escapeHtml(tag)}</span>`
|
|
||||||
+ `</div>`;
|
|
||||||
}).join("");
|
|
||||||
}
|
|
||||||
function show() { activeIdx = -1; render(); panel.classList.add("show"); }
|
|
||||||
function hide() { panel.classList.remove("show"); activeIdx = -1; }
|
|
||||||
function pick(idx) {
|
|
||||||
const f = filtered[idx];
|
|
||||||
if (!f) return;
|
|
||||||
input.value = f.name;
|
|
||||||
hide();
|
|
||||||
if (onPick) onPick(f.name);
|
|
||||||
}
|
|
||||||
// 内部:打字 → 实时过滤 + 保持显示(外部 input listener 仍可注册业务副作用,顺序无依赖)
|
|
||||||
input.addEventListener("input", () => { activeIdx = -1; render(); panel.classList.add("show"); });
|
|
||||||
input.addEventListener("focus", show);
|
|
||||||
input.addEventListener("click", show);
|
|
||||||
// blur 延迟关闭,让 panel item 的 mousedown 来得及触发(mousedown 也 preventDefault 双保险)
|
|
||||||
input.addEventListener("blur", () => setTimeout(hide, 120));
|
|
||||||
panel.addEventListener("mousedown", (e) => {
|
|
||||||
const item = e.target.closest(".item");
|
|
||||||
if (!item) return;
|
|
||||||
e.preventDefault(); // 阻止 input 失焦提前关 panel
|
|
||||||
pick(parseInt(item.dataset.idx));
|
|
||||||
});
|
|
||||||
// 键盘:↑↓ 移动 active,Enter 选中,Esc 关闭
|
|
||||||
input.addEventListener("keydown", (e) => {
|
|
||||||
if (!panel.classList.contains("show")) return;
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (filtered.length === 0) return;
|
|
||||||
activeIdx = (activeIdx + 1) % filtered.length;
|
|
||||||
render();
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (filtered.length === 0) return;
|
|
||||||
activeIdx = activeIdx <= 0 ? filtered.length - 1 : activeIdx - 1;
|
|
||||||
render();
|
|
||||||
} else if (e.key === "Enter" && activeIdx >= 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
pick(activeIdx);
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { show, hide, render };
|
|
||||||
}
|
|
||||||
|
|
||||||
// modal 新建任务的工作目录:选中后置 manual flag + 刷新 hint
|
|
||||||
const wdCombo = makeFolderCombo({
|
|
||||||
input: $("nt-wd-sel"),
|
|
||||||
panel: $("nt-wd-panel"),
|
|
||||||
onPick: () => { wdManuallyEdited = true; updateWdHint(); },
|
|
||||||
});
|
|
||||||
// 顶部 filter-wd:选中后立即触发筛选(打字仍走 scheduleFilter debounce)
|
|
||||||
makeFolderCombo({
|
|
||||||
input: $("filter-wd"),
|
|
||||||
panel: $("filter-wd-panel"),
|
|
||||||
onPick: () => loadTaskList(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// name 改变 → 若 wd 未被手动改,同步;并刷新 hint
|
|
||||||
$("nt-name").addEventListener("input", () => {
|
$("nt-name").addEventListener("input", () => {
|
||||||
if (!wdManuallyEdited) $("nt-wd-sel").value = $("nt-name").value;
|
updateSentinelLabel();
|
||||||
|
if (!wdManuallyEdited && $("nt-wd-sel").value === "__new__") {
|
||||||
|
$("nt-wd-new").value = $("nt-name").value;
|
||||||
|
}
|
||||||
updateWdHint();
|
updateWdHint();
|
||||||
});
|
});
|
||||||
// wd 改变(用户打字) → 非空视为手动修改;清空 → 重置 flag 但保持空(避免 backspace 想换名字时被打断)
|
|
||||||
$("nt-wd-sel").addEventListener("input", () => {
|
// wd select 切换 → 切显示二级 input + 刷 hint
|
||||||
wdManuallyEdited = $("nt-wd-sel").value.trim() !== "";
|
$("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();
|
updateWdHint();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue