|
|
@@ -232,6 +232,21 @@
|
|
|
border-color: #a7d9b4;
|
|
|
color: var(--green);
|
|
|
}
|
|
|
+ .pill.blue {
|
|
|
+ background: var(--blue-bg);
|
|
|
+ border-color: #b6cdf7;
|
|
|
+ color: var(--blue);
|
|
|
+ }
|
|
|
+ /* 目的列 intent 胶囊: 底色 = 所引用列的分组色, 与表头/列 chip 一致
|
|
|
+ (需求=navy / 输入=amber / 实现=teal / 输出=green; 口径同 procedure-dsl「token 色对应来源列」) */
|
|
|
+ .intent-text { color: var(--ink); line-height: 1.6; }
|
|
|
+ .intent-tok { display: inline-block; padding: 1px 6px; border-radius: 4px; margin: 0 1px; font-size: 11.5px; font-weight: 500; }
|
|
|
+ .intent-tok.ik-effect { background: #e8eef6; color: var(--navy); } /* 作用列 (需求组) */
|
|
|
+ .intent-tok.ik-via { background: var(--teal-bg); color: var(--teal); font-family: "IBM Plex Mono", ui-monospace, monospace; } /* 外部工具列 (实现组) */
|
|
|
+ .intent-tok.ik-act { background: var(--teal-bg); color: var(--teal); } /* 动作列 (实现组) */
|
|
|
+ .intent-tok.ik-in-type { background: var(--amber-bg); color: var(--amber); border: 1px solid #ecc88a; border-radius: 99px; padding: 1px 8px; } /* 输入·类型 */
|
|
|
+ .intent-tok.ik-out-type { background: var(--blue-bg); color: var(--blue); border: 1px solid #b6cdf7; border-radius: 99px; padding: 1px 8px; } /* 输出·类型 (蓝色,避免与实现组绿色混淆) */
|
|
|
+ .intent-tok.ik-other { background: #fbeae5; color: var(--seal); text-decoration: line-through; } /* 非法类别(lint 警告) */
|
|
|
.btn {
|
|
|
border: 1px solid var(--line-dark);
|
|
|
background: var(--card);
|
|
|
@@ -917,10 +932,10 @@
|
|
|
background: #2d8273;
|
|
|
}
|
|
|
.steps .h-out {
|
|
|
- background: var(--green);
|
|
|
+ background: var(--blue);
|
|
|
}
|
|
|
.steps .h-out2 {
|
|
|
- background: #2e6b45;
|
|
|
+ background: #4f7fe6;
|
|
|
}
|
|
|
.steps td {
|
|
|
padding: 8px 9px;
|
|
|
@@ -935,7 +950,7 @@
|
|
|
background: var(--amber-bg) !important;
|
|
|
}
|
|
|
.steps td.c-out {
|
|
|
- background: var(--green-bg) !important;
|
|
|
+ background: var(--blue-bg) !important;
|
|
|
}
|
|
|
.steps .sid {
|
|
|
font-family: "IBM Plex Mono", monospace;
|
|
|
@@ -993,7 +1008,7 @@
|
|
|
background: linear-gradient(180deg, rgba(255, 247, 232, 0), rgba(255, 247, 232, 1));
|
|
|
}
|
|
|
.steps td.c-out .clamp-val.clampable::after {
|
|
|
- background: linear-gradient(180deg, rgba(239, 250, 241, 0), rgba(239, 250, 241, 1));
|
|
|
+ background: linear-gradient(180deg, rgba(238, 243, 254, 0), rgba(238, 243, 254, 1));
|
|
|
}
|
|
|
.clamp-val.open {
|
|
|
max-height: none;
|
|
|
@@ -1166,6 +1181,84 @@
|
|
|
background: rgba(19, 30, 46, 0.5);
|
|
|
backdrop-filter: blur(2px);
|
|
|
}
|
|
|
+ /* 重新解构·编辑 Prompt 弹框 */
|
|
|
+ dialog#reextract-dlg {
|
|
|
+ border: none;
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 0;
|
|
|
+ width: min(680px, 94vw);
|
|
|
+ box-shadow: var(--shadow-lg);
|
|
|
+ margin: auto;
|
|
|
+ }
|
|
|
+ dialog#reextract-dlg::backdrop {
|
|
|
+ background: rgba(19, 30, 46, 0.42);
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
+ }
|
|
|
+ .rx-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ max-height: calc(100vh - 48px);
|
|
|
+ }
|
|
|
+ .rx-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 14px 18px 12px;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ }
|
|
|
+ .rx-title {
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 15px;
|
|
|
+ color: var(--navy-deep);
|
|
|
+ }
|
|
|
+ .rx-sub {
|
|
|
+ padding: 12px 18px 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+ .rx-model {
|
|
|
+ padding: 12px 18px 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .rx-model label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--ink);
|
|
|
+ }
|
|
|
+ .rx-prompt {
|
|
|
+ margin: 12px 18px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 300px;
|
|
|
+ resize: vertical;
|
|
|
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: var(--ink);
|
|
|
+ border: 1px solid var(--line-dark);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #fbfaf6;
|
|
|
+ }
|
|
|
+ .rx-foot {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 12px 18px 16px;
|
|
|
+ border-top: 1px solid var(--line);
|
|
|
+ }
|
|
|
+ #rx-save {
|
|
|
+ background: var(--green);
|
|
|
+ border-color: var(--green);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ #rx-save:hover {
|
|
|
+ background: #126b33;
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 4px 12px rgba(21, 128, 61, 0.3);
|
|
|
+ }
|
|
|
.pd-wrap {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
@@ -1423,6 +1516,17 @@
|
|
|
color: var(--blue);
|
|
|
text-decoration: none;
|
|
|
}
|
|
|
+ .src-case {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ font-size: 11px;
|
|
|
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
|
+ color: var(--ink-faint);
|
|
|
+ background: var(--paper, #f3f2ed);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ padding: 1px 7px;
|
|
|
+ border-radius: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
.src-body {
|
|
|
padding: 0 12px 12px;
|
|
|
}
|
|
|
@@ -2135,6 +2239,49 @@
|
|
|
</div>
|
|
|
</dialog>
|
|
|
|
|
|
+ <!-- 重新解构 · 编辑解构 Prompt -->
|
|
|
+ <dialog id="reextract-dlg">
|
|
|
+ <div class="rx-wrap">
|
|
|
+ <div class="rx-head">
|
|
|
+ <div class="rx-title">重新解构 · 编辑解构 Prompt</div>
|
|
|
+ <button
|
|
|
+ class="btn sm"
|
|
|
+ onclick="document.getElementById('reextract-dlg').close()"
|
|
|
+ >
|
|
|
+ 关闭
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="rx-sub">
|
|
|
+ 修改下面的解构 Prompt 后,点「保存修改」将按最新 Prompt
|
|
|
+ 重新解构本帖(生成新版本,旧版本保留)。改动仅本次生效,不会改写默认 Prompt。
|
|
|
+ </div>
|
|
|
+ <div class="rx-model">
|
|
|
+ <label>模型:</label>
|
|
|
+ <select id="rx-model"></select>
|
|
|
+ </div>
|
|
|
+ <textarea
|
|
|
+ id="rx-prompt"
|
|
|
+ class="rx-prompt"
|
|
|
+ spellcheck="false"
|
|
|
+ ></textarea>
|
|
|
+ <div class="rx-foot">
|
|
|
+ <button
|
|
|
+ class="btn"
|
|
|
+ onclick="document.getElementById('reextract-dlg').close()"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn primary"
|
|
|
+ id="rx-save"
|
|
|
+ onclick="saveReextract()"
|
|
|
+ >
|
|
|
+ 保存修改
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </dialog>
|
|
|
+
|
|
|
<!-- 图片预览灯箱(支持左右切换) -->
|
|
|
<dialog
|
|
|
class="lightbox"
|
|
|
@@ -2274,6 +2421,25 @@
|
|
|
/[&<>"']/g,
|
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
|
|
|
);
|
|
|
+ /* 目的列: 把 intent 里的 {类别:值} 标记渲染成彩色胶囊 (口径同 procedure-dsl renderer.py:render_intent)。
|
|
|
+ 合法类别 5 个: effect/via/act/in-type/out-type; 其余落 ik-other(红删除线,当 lint 警告)。
|
|
|
+ 标记外的字面文字与值都做 HTML 转义。 */
|
|
|
+ const INTENT_KIND = {
|
|
|
+ effect: "ik-effect", via: "ik-via", act: "ik-act",
|
|
|
+ "in-type": "ik-in-type", "out-type": "ik-out-type",
|
|
|
+ };
|
|
|
+ const renderIntent = (text) => {
|
|
|
+ const s = String(text ?? "");
|
|
|
+ const re = /\{([\w-]+):([^}]+)\}/g;
|
|
|
+ let out = "", last = 0, m;
|
|
|
+ while ((m = re.exec(s))) {
|
|
|
+ out += esc(s.slice(last, m.index).replace(/`/g, ""));
|
|
|
+ const kc = INTENT_KIND[m[1]] || "ik-other";
|
|
|
+ out += `<span class="intent-tok ${kc}">${esc(m[2])}</span>`;
|
|
|
+ last = m.index + m[0].length;
|
|
|
+ }
|
|
|
+ return out + esc(s.slice(last).replace(/`/g, ""));
|
|
|
+ };
|
|
|
/* 外链图片走本服务同源反代,绕过公众号(mmbiz.qpic.cn)等防盗链 */
|
|
|
const imgProxy = (u) => (/^https?:\/\//.test(u || "") ? "/api/img?u=" + encodeURIComponent(u) : u || "");
|
|
|
/* 图片加载策略:优先「浏览器直连 CDN」(referrerpolicy=no-referrer 多数能绕防盗链),
|
|
|
@@ -2869,6 +3035,7 @@
|
|
|
if (p.like_count != null) meta.push(`<span>👍 ${p.like_count}</span>`);
|
|
|
if (p.quality_grade) meta.push(`<span>质量 ${esc(p.quality_grade)} ${p.quality_score ?? ""}</span>`);
|
|
|
if (p.url) meta.push(`<a href="${esc(p.url)}" target="_blank">原文 ↗</a>`);
|
|
|
+ meta.push(`<span class="pill" title="case_id" style="font-family:'IBM Plex Mono',ui-monospace,monospace">${esc(p.case_id || cid)}</span>`);
|
|
|
$("#pd-meta").innerHTML = meta.join("");
|
|
|
$("#pd-title").textContent = p.title || "(无标题)";
|
|
|
const verdict = e["判定理由"] || e["理由"] || "";
|
|
|
@@ -3008,15 +3175,13 @@
|
|
|
`<option value="${esc(v.version)}">${esc(v.version)}${i === 0 ? " (最新)" : ""} · ${v.n}${isProc ? "工序" : "工具"}</option>`,
|
|
|
)
|
|
|
.join("");
|
|
|
- const models = (isProc ? MODELS_PROC : MODELS_TOOL).map((m) => `<option>${m}</option>`).join("");
|
|
|
const stat = loading ? "加载中…" : missing ? "未提取" : "已提取";
|
|
|
$("#xp-head").innerHTML = `
|
|
|
<span class="st">大模型${isProc ? "工序" : "工具"}:<em>${stat}</em></span>
|
|
|
<span style="font-size:12px;color:var(--ink-faint);max-width:330px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title}</span>
|
|
|
<span class="spacer"></span>
|
|
|
${versions.length ? `<select id="ver-sel">${opts}</select>` : ""}
|
|
|
- <select id="model-sel" title="解构模型">${models}</select>
|
|
|
- <button class="btn sm primary" onclick="startExtract(['${esc(state.caseId || "")}'])">${missing ? "提取" : "♻ 重新生成"}</button>
|
|
|
+ <button class="btn sm primary" onclick="openReextractDialog()">${missing ? "提取" : "♻ 重新生成"}</button>
|
|
|
<button class="btn sm" onclick="showTaskPanel()" title="重新打开任务日志面板">📋 操作日志</button>`;
|
|
|
const vs = $("#ver-sel");
|
|
|
if (vs)
|
|
|
@@ -3058,6 +3223,7 @@
|
|
|
<span class="src-label">原文</span>
|
|
|
<span class="src-title">${esc(p.title || "(无标题)")}</span>
|
|
|
${link}
|
|
|
+ <span class="src-case" title="case_id" onclick="event.stopPropagation()">${esc(p.case_id || "")}</span>
|
|
|
</div>
|
|
|
<div class="src-body">${thumbs}${body}</div>
|
|
|
</div>`;
|
|
|
@@ -3142,7 +3308,7 @@
|
|
|
rows += "<tr>";
|
|
|
if (i === 0) {
|
|
|
rows += `<td rowspan="${n}" class="sid">${esc(s.id || "")}</td>
|
|
|
- <td rowspan="${n}">${esc(s.directive || s.intent || "")}</td>
|
|
|
+ <td rowspan="${n}"><div class="intent-text">${renderIntent(s.intent || s.directive || "")}</div></td>
|
|
|
<td rowspan="${n}">${s.effect ? `<span class="pill navy">${esc(s.effect)}</span>` : ""}</td>
|
|
|
<td rowspan="${n}">${esc(fmtSF(s.substance))}</td>
|
|
|
<td rowspan="${n}">${esc(fmtSF(s.form))}</td>`;
|
|
|
@@ -3182,7 +3348,7 @@
|
|
|
if (!x) return `<td class="${cls}"></td><td class="${cls}"></td><td class="${cls}"></td>`;
|
|
|
const inf = x.inferred ? ` inf" title="推断理由:${esc(x.inferred_reason || "模型推断补全")}` : "";
|
|
|
const badge = x.inferred ? '<span class="ib">推</span>' : "";
|
|
|
- return `<td class="${cls}"><span class="pill ${kind === "in" ? "amber" : "teal"}">${esc(x.type || "")}</span></td>
|
|
|
+ return `<td class="${cls}"><span class="pill ${kind === "in" ? "amber" : "blue"}">${esc(x.type || "")}</span></td>
|
|
|
<td class="${cls}${inf}">${badge}<div class="clamp-val" onclick="toggleClampVal(this)"><span class="vtxt">${esc(x.value || "")}</span></div></td>
|
|
|
<td class="${cls}"><span class="anchor">${esc(x.anchor || "")}</span></td>`;
|
|
|
}
|
|
|
@@ -3296,15 +3462,55 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /* ════ 重新解构 · 编辑 Prompt 弹框 ════ */
|
|
|
+ async function openReextractDialog() {
|
|
|
+ if (!state.caseId) {
|
|
|
+ toast("请先选择帖子", "info");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const isProc = state.mode === "process";
|
|
|
+ const dlg = $("#reextract-dlg"),
|
|
|
+ sel = $("#rx-model"),
|
|
|
+ ta = $("#rx-prompt"),
|
|
|
+ save = $("#rx-save");
|
|
|
+ sel.innerHTML = (isProc ? MODELS_PROC : MODELS_TOOL)
|
|
|
+ .map((m) => `<option>${m}</option>`)
|
|
|
+ .join("");
|
|
|
+ ta.value = "加载中…";
|
|
|
+ ta.disabled = save.disabled = true;
|
|
|
+ dlg.showModal();
|
|
|
+ try {
|
|
|
+ const r = await api(`/api/extract_prompt?mode=${state.mode}`);
|
|
|
+ ta.value = r.prompt || "";
|
|
|
+ } catch (e) {
|
|
|
+ ta.value = "";
|
|
|
+ toast("加载 Prompt 失败:" + (e.body?.error || e.status), "error");
|
|
|
+ } finally {
|
|
|
+ ta.disabled = save.disabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async function saveReextract() {
|
|
|
+ const cid = state.caseId;
|
|
|
+ if (!cid) return;
|
|
|
+ const model = $("#rx-model").value,
|
|
|
+ prompt = $("#rx-prompt").value;
|
|
|
+ $("#reextract-dlg").close();
|
|
|
+ // 临时 prompt 覆盖 + force 重解构(仅本次生效,不改默认 Prompt)
|
|
|
+ await startExtract([cid], { model, prompt, force: true });
|
|
|
+ }
|
|
|
+
|
|
|
/* ════ 解构任务 ════ */
|
|
|
- async function startExtract(caseIds) {
|
|
|
+ async function startExtract(caseIds, opts = {}) {
|
|
|
if (!state.queryId || !caseIds.length) return;
|
|
|
const isProc = state.mode === "process";
|
|
|
- const model = $("#model-sel")?.value || (isProc ? MODELS_PROC[0] : MODELS_TOOL[0]);
|
|
|
+ const model = opts.model || (isProc ? MODELS_PROC[0] : MODELS_TOOL[0]);
|
|
|
try {
|
|
|
+ const body = { query_id: state.queryId, case_ids: caseIds, model };
|
|
|
+ if (opts.prompt != null) body.prompt = opts.prompt; // 临时 prompt 覆盖(仅本次)
|
|
|
+ if (opts.force) body.force = true; // 换 prompt/模型重解构需跳过去重
|
|
|
const r = await api(`/api/extract_${isProc ? "process" : "tools"}`, {
|
|
|
method: "POST",
|
|
|
- body: JSON.stringify({ query_id: state.queryId, case_ids: caseIds, model }),
|
|
|
+ body: JSON.stringify(body),
|
|
|
});
|
|
|
// 全部正在解构中(被认领跳过):没有 task_id,提示一下即可,别去轮询空任务
|
|
|
if (!r.task_id) {
|