feat(ui): add deep extraction preview panel with API/column/static-params display

This commit is contained in:
木炎
2026-04-17 10:37:50 +08:00
parent 689abf08ec
commit 7289cc5779

View File

@@ -114,6 +114,102 @@
.hint { font-size: 0.8rem; color: var(--muted); margin-top: 4px; } .hint { font-size: 0.8rem; color: var(--muted); margin-top: 4px; }
.divider { height: 1px; background: var(--line); margin: 12px 0; } .divider { height: 1px; background: var(--line); margin: 12px 0; }
@media (max-width: 900px) { body { padding: 16px; } .content { grid-template-columns: 1fr; } .sidebar { border-right: 0; border-bottom: 1px solid var(--line); } .stream { max-height: none; } } @media (max-width: 900px) { body { padding: 16px; } .content { grid-template-columns: 1fr; } .sidebar { border-right: 0; border-bottom: 1px solid var(--line); } .stream { max-height: none; } }
/* Preview panel styles */
.preview-panel {
background: rgba(255, 255, 255, 0.6);
border-radius: 16px;
border: 1px solid var(--line);
overflow: hidden;
margin-top: 16px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
cursor: pointer;
background: rgba(255, 255, 255, 0.5);
}
.preview-header h3 {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
color: var(--text);
}
.preview-header:hover {
background: rgba(255, 255, 255, 0.7);
}
.preview-content {
padding: 16px;
}
.preview-section {
margin-bottom: 16px;
}
.preview-section:last-child {
margin-bottom: 0;
}
.preview-section h4 {
margin: 0 0 8px 0;
font-size: 0.85rem;
font-weight: 700;
color: var(--accent);
}
.preview-row {
display: flex;
margin-bottom: 6px;
}
.preview-row .label {
min-width: 80px;
color: var(--muted);
flex-shrink: 0;
font-size: 0.88rem;
}
.preview-row .value {
color: var(--text);
font-size: 0.88rem;
}
.preview-list {
max-height: 120px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.8);
border-radius: 12px;
padding: 8px 12px;
border: 1px solid var(--line);
}
.preview-list-item {
padding: 5px 0;
border-bottom: 1px solid var(--line);
font-size: 0.85rem;
}
.preview-list-item:last-child {
border-bottom: none;
}
.preview-code {
background: rgba(0, 0, 0, 0.04);
padding: 10px;
border-radius: 10px;
font-family: "Consolas", "Monaco", monospace;
font-size: 0.8rem;
overflow-x: auto;
white-space: pre-wrap;
color: var(--text);
}
.btn-group {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn-group button {
flex: 1;
}
.secondary-btn {
background: rgba(15, 118, 110, 0.1);
color: var(--accent);
border: 1px solid rgba(15, 118, 110, 0.3);
}
.secondary-btn:hover:not(:disabled) {
background: rgba(15, 118, 110, 0.15);
}
</style> </style>
</head> </head>
<body> <body>
@@ -157,6 +253,67 @@
<input id="targetUrl" placeholder="例如http://20.76.57.61:18080/report" /> <input id="targetUrl" placeholder="例如http://20.76.57.61:18080/report" />
<p class="hint">场景要访问的目标页面地址,留空则使用自动提取的域名</p> <p class="hint">场景要访问的目标页面地址,留空则使用自动提取的域名</p>
</div> </div>
<div class="btn-group" style="margin-top: 8px; margin-bottom: 16px;">
<button id="analyzeBtn" class="secondary-btn" onclick="analyzeDeep()">深度分析</button>
</div>
<!-- 提取结果预览 -->
<div id="extractionPreview" class="preview-panel" style="display: none;">
<div class="preview-header" onclick="togglePreview()">
<h3>LLM 提取结果</h3>
<span id="previewToggleIcon"></span>
</div>
<div id="previewContent" class="preview-content">
<div class="preview-section">
<h4>基本信息</h4>
<div class="preview-row">
<span class="label">场景 ID:</span>
<span id="previewSceneId" class="value"></span>
</div>
<div class="preview-row">
<span class="label">场景名称:</span>
<span id="previewSceneName" class="value"></span>
</div>
<div class="preview-row">
<span class="label">场景类型:</span>
<span id="previewSceneKind" class="value"></span>
</div>
<div class="preview-row">
<span class="label">目标域名:</span>
<span id="previewExpectedDomain" class="value"></span>
</div>
</div>
<div class="preview-section">
<h4>API 端点 (<span id="previewApiCount">0</span>)</h4>
<div id="previewApiEndpoints" class="preview-list"></div>
</div>
<div class="preview-section">
<h4>列定义 (<span id="previewColumnCount">0</span>)</h4>
<div id="previewColumnDefs" class="preview-list"></div>
</div>
<div class="preview-section">
<h4>静态参数</h4>
<pre id="previewStaticParams" class="preview-code"></pre>
</div>
<div class="preview-section">
<h4>业务逻辑</h4>
<div class="preview-row">
<span class="label">数据获取:</span>
<span id="previewDataFetch" class="value"></span>
</div>
<div class="preview-row">
<span class="label">数据转换:</span>
<span id="previewDataTransform" class="value"></span>
</div>
</div>
</div>
</div>
<div class="divider"></div> <div class="divider"></div>
<p class="section-label">Settings</p> <p class="section-label">Settings</p>
<div class="field"> <div class="field">
@@ -166,13 +323,6 @@
<button id="browseOutputRoot" class="ghost-btn browse-btn">浏览</button> <button id="browseOutputRoot" class="ghost-btn browse-btn">浏览</button>
</div> </div>
</div> </div>
<div class="field">
<label>Lessons 路径</label>
<div class="input-row">
<input id="settingLessons" placeholder="点击浏览选择文件..." readonly />
<button id="browseLessons" class="ghost-btn browse-btn">浏览</button>
</div>
</div>
<div id="validationText" class="validation"></div> <div id="validationText" class="validation"></div>
<button id="generateBtn" class="primary-btn" disabled>生成 Skill</button> <button id="generateBtn" class="primary-btn" disabled>生成 Skill</button>
</div> </div>
@@ -201,9 +351,7 @@
targetUrl: document.getElementById("targetUrl"), targetUrl: document.getElementById("targetUrl"),
browseSourceDir: document.getElementById("browseSourceDir"), browseSourceDir: document.getElementById("browseSourceDir"),
browseOutputRoot: document.getElementById("browseOutputRoot"), browseOutputRoot: document.getElementById("browseOutputRoot"),
browseLessons: document.getElementById("browseLessons"),
settingOutputRoot: document.getElementById("settingOutputRoot"), settingOutputRoot: document.getElementById("settingOutputRoot"),
settingLessons: document.getElementById("settingLessons"),
generateBtn: document.getElementById("generateBtn"), generateBtn: document.getElementById("generateBtn"),
validationText: document.getElementById("validationText"), validationText: document.getElementById("validationText"),
stateChip: document.getElementById("stateChip"), stateChip: document.getElementById("stateChip"),
@@ -212,6 +360,8 @@
emptyState: document.getElementById("emptyState"), emptyState: document.getElementById("emptyState"),
}; };
let defaultsLoaded = false; let defaultsLoaded = false;
let currentSceneInfo = null; // Stores deep extraction results
let previewExpanded = false;
function setState(state, text) { function setState(state, text) {
els.stateChip.textContent = text; els.stateChip.textContent = text;
@@ -281,7 +431,6 @@
if (health.projectRoot) { if (health.projectRoot) {
const root = health.projectRoot.replace(/\\/g, "/"); const root = health.projectRoot.replace(/\\/g, "/");
els.settingOutputRoot.value = root + "/examples/generated_scene_platform"; els.settingOutputRoot.value = root + "/examples/generated_scene_platform";
els.settingLessons.value = root + "/docs/superpowers/references/tq-lineloss-lessons-learned.toml";
} }
updateGenerateBtn(); updateGenerateBtn();
} catch (err) { } catch (err) {
@@ -291,6 +440,144 @@
} }
} }
async function analyzeSourceDir(sourceDir) {
if (!sourceDir) return;
setState("analyzing", "正在分析场景目录...");
appendRow("status", "开始分析场景目录...");
try {
const res = await fetch(`${SERVER_URL}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceDir }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Analyze failed");
if (data.sceneId) {
els.sceneId.value = data.sceneId;
}
if (data.sceneName) {
els.sceneName.value = data.sceneName;
}
appendRow("status", `分析完成: ${data.sceneId || ""} ${data.sceneName || ""}`.trim());
} catch (err) {
appendRow("error", `分析失败: ${err.message}`);
} finally {
setState("ready", "就绪");
updateGenerateBtn();
}
}
function togglePreview() {
const content = document.getElementById("previewContent");
const icon = document.getElementById("previewToggleIcon");
previewExpanded = !previewExpanded;
content.style.display = previewExpanded ? "block" : "none";
icon.textContent = previewExpanded ? "▲" : "▼";
}
function showExtractionPreview(data) {
const panel = document.getElementById("extractionPreview");
panel.style.display = "block";
previewExpanded = true;
document.getElementById("previewContent").style.display = "block";
document.getElementById("previewToggleIcon").textContent = "▲";
// Basic info
document.getElementById("previewSceneId").textContent = data.sceneId || "-";
document.getElementById("previewSceneName").textContent = data.sceneName || "-";
document.getElementById("previewSceneKind").textContent = data.sceneKind || "-";
document.getElementById("previewExpectedDomain").textContent = data.expectedDomain || "-";
// API endpoints
const apiList = document.getElementById("previewApiEndpoints");
const apiCount = document.getElementById("previewApiCount");
if (data.apiEndpoints && data.apiEndpoints.length > 0) {
apiCount.textContent = data.apiEndpoints.length;
apiList.innerHTML = data.apiEndpoints.map(ep =>
`<div class="preview-list-item">${escapeHtml(ep)}</div>`
).join("");
} else {
apiCount.textContent = "0";
apiList.innerHTML = '<div class="preview-list-item" style="color: var(--muted);">无</div>';
}
// Column definitions
const colList = document.getElementById("previewColumnDefs");
const colCount = document.getElementById("previewColumnCount");
if (data.columnDefs && data.columnDefs.length > 0) {
colCount.textContent = data.columnDefs.length;
colList.innerHTML = data.columnDefs.map(col =>
`<div class="preview-list-item">${escapeHtml(col)}</div>`
).join("");
} else {
colCount.textContent = "0";
colList.innerHTML = '<div class="preview-list-item" style="color: var(--muted);">无</div>';
}
// Static params
const staticParams = document.getElementById("previewStaticParams");
if (data.staticParams) {
staticParams.textContent = JSON.stringify(data.staticParams, null, 2);
} else {
staticParams.textContent = "{}";
}
// Business logic
const biz = data.businessLogic || {};
document.getElementById("previewDataFetch").textContent = biz.dataFetch || "-";
document.getElementById("previewDataTransform").textContent = biz.dataTransform || "-";
}
function escapeHtml(str) {
if (!str) return "";
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
async function analyzeDeep() {
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
if (!sourceDir) {
setValidation("请先选择场景目录");
return;
}
setValidation("");
setState("analyzing", "正在深度分析...");
appendRow("status", "开始深度分析场景...");
try {
const res = await fetch(`${SERVER_URL}/analyze-deep`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceDir }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Deep analysis failed");
// Store the scene info for generation
currentSceneInfo = data;
// Fill in form fields if not already set
if (data.sceneId && !els.sceneId.value.trim()) {
els.sceneId.value = data.sceneId;
}
if (data.sceneName && !els.sceneName.value.trim()) {
els.sceneName.value = data.sceneName;
}
// Show preview
showExtractionPreview(data);
appendRow("status", `深度分析完成: 找到 ${data.apiEndpoints?.length || 0} 个 API 端点, ${data.columnDefs?.length || 0} 个列定义`);
} catch (err) {
appendRow("error", `深度分析失败: ${err.message}`);
} finally {
setState("ready", "就绪");
updateGenerateBtn();
}
}
async function generate() { async function generate() {
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/"); const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
const sceneId = els.sceneId.value.trim(); const sceneId = els.sceneId.value.trim();
@@ -298,18 +585,29 @@
const sceneKind = els.sceneKind.value; const sceneKind = els.sceneKind.value;
const targetUrl = els.targetUrl.value.trim(); const targetUrl = els.targetUrl.value.trim();
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/"); const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
const lessons = els.settingLessons.value.trim().replace(/\\/g, "/"); if (!sourceDir || !sceneId || !sceneName || !outputRoot) { setValidation("场景目录、scene-id、scene-name、输出根路径为必填"); return; }
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { setValidation("所有字段均为必填"); return; }
setValidation(""); setValidation("");
setState("generating", "正在生成 skill 包..."); setState("generating", "正在生成 skill 包...");
els.generateBtn.disabled = true; els.generateBtn.disabled = true;
appendRow("status", "开始生成 skill 包..."); appendRow("status", "开始生成 skill 包...");
try { try {
const requestBody = {
sourceDir,
sceneId,
sceneName,
sceneKind,
targetUrl: targetUrl || null,
outputRoot,
};
// Include deep extraction results if available
if (currentSceneInfo) {
requestBody.sceneInfoJson = JSON.stringify(currentSceneInfo);
}
const res = await fetch(`${SERVER_URL}/generate`, { const res = await fetch(`${SERVER_URL}/generate`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceDir, sceneId, sceneName, sceneKind, targetUrl: targetUrl || null, outputRoot, lessons }), body: JSON.stringify(requestBody),
}); });
if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); } if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); }
@@ -359,13 +657,13 @@
const path = await selectFolder(els.sourceDir.value || null); const path = await selectFolder(els.sourceDir.value || null);
if (path) { if (path) {
els.sourceDir.value = path; els.sourceDir.value = path;
// Auto-fill scene-id from folder name
const parts = path.replace(/\\/g, "/").split("/"); const parts = path.replace(/\\/g, "/").split("/");
const folderName = parts[parts.length - 1]; const folderName = parts[parts.length - 1];
if (folderName && !els.sceneId.value) { if (folderName && !els.sceneId.value) {
els.sceneId.value = folderName; els.sceneId.value = folderName;
} }
updateGenerateBtn(); updateGenerateBtn();
await analyzeSourceDir(path.replace(/\\/g, "/"));
} }
}); });
@@ -377,14 +675,6 @@
} }
}); });
els.browseLessons.addEventListener("click", async () => {
const path = await selectFile(els.settingLessons.value || null, "TOML 文件 (*.toml)|*.toml|所有文件 (*.*)|*.*");
if (path) {
els.settingLessons.value = path;
updateGenerateBtn();
}
});
els.generateBtn.addEventListener("click", generate); els.generateBtn.addEventListener("click", generate);
els.sceneId.addEventListener("input", updateGenerateBtn); els.sceneId.addEventListener("input", updateGenerateBtn);
els.sceneName.addEventListener("input", updateGenerateBtn); els.sceneName.addEventListener("input", updateGenerateBtn);