feat(ui): add deep extraction preview panel with API/column/static-params display
This commit is contained in:
@@ -114,6 +114,102 @@
|
||||
.hint { font-size: 0.8rem; color: var(--muted); margin-top: 4px; }
|
||||
.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; } }
|
||||
/* 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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -157,6 +253,67 @@
|
||||
<input id="targetUrl" placeholder="例如:http://20.76.57.61:18080/report" />
|
||||
<p class="hint">场景要访问的目标页面地址,留空则使用自动提取的域名</p>
|
||||
</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>
|
||||
<p class="section-label">Settings</p>
|
||||
<div class="field">
|
||||
@@ -166,13 +323,6 @@
|
||||
<button id="browseOutputRoot" class="ghost-btn browse-btn">浏览</button>
|
||||
</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>
|
||||
<button id="generateBtn" class="primary-btn" disabled>生成 Skill</button>
|
||||
</div>
|
||||
@@ -201,9 +351,7 @@
|
||||
targetUrl: document.getElementById("targetUrl"),
|
||||
browseSourceDir: document.getElementById("browseSourceDir"),
|
||||
browseOutputRoot: document.getElementById("browseOutputRoot"),
|
||||
browseLessons: document.getElementById("browseLessons"),
|
||||
settingOutputRoot: document.getElementById("settingOutputRoot"),
|
||||
settingLessons: document.getElementById("settingLessons"),
|
||||
generateBtn: document.getElementById("generateBtn"),
|
||||
validationText: document.getElementById("validationText"),
|
||||
stateChip: document.getElementById("stateChip"),
|
||||
@@ -212,6 +360,8 @@
|
||||
emptyState: document.getElementById("emptyState"),
|
||||
};
|
||||
let defaultsLoaded = false;
|
||||
let currentSceneInfo = null; // Stores deep extraction results
|
||||
let previewExpanded = false;
|
||||
|
||||
function setState(state, text) {
|
||||
els.stateChip.textContent = text;
|
||||
@@ -281,7 +431,6 @@
|
||||
if (health.projectRoot) {
|
||||
const root = health.projectRoot.replace(/\\/g, "/");
|
||||
els.settingOutputRoot.value = root + "/examples/generated_scene_platform";
|
||||
els.settingLessons.value = root + "/docs/superpowers/references/tq-lineloss-lessons-learned.toml";
|
||||
}
|
||||
updateGenerateBtn();
|
||||
} 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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() {
|
||||
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
|
||||
const sceneId = els.sceneId.value.trim();
|
||||
@@ -298,18 +585,29 @@
|
||||
const sceneKind = els.sceneKind.value;
|
||||
const targetUrl = els.targetUrl.value.trim();
|
||||
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
|
||||
const lessons = els.settingLessons.value.trim().replace(/\\/g, "/");
|
||||
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { setValidation("所有字段均为必填"); return; }
|
||||
if (!sourceDir || !sceneId || !sceneName || !outputRoot) { setValidation("场景目录、scene-id、scene-name、输出根路径为必填"); return; }
|
||||
setValidation("");
|
||||
setState("generating", "正在生成 skill 包...");
|
||||
els.generateBtn.disabled = true;
|
||||
appendRow("status", "开始生成 skill 包...");
|
||||
|
||||
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`, {
|
||||
method: "POST",
|
||||
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"); }
|
||||
|
||||
@@ -359,13 +657,13 @@
|
||||
const path = await selectFolder(els.sourceDir.value || null);
|
||||
if (path) {
|
||||
els.sourceDir.value = path;
|
||||
// Auto-fill scene-id from folder name
|
||||
const parts = path.replace(/\\/g, "/").split("/");
|
||||
const folderName = parts[parts.length - 1];
|
||||
if (folderName && !els.sceneId.value) {
|
||||
els.sceneId.value = folderName;
|
||||
}
|
||||
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.sceneId.addEventListener("input", updateGenerateBtn);
|
||||
els.sceneName.addEventListener("input", updateGenerateBtn);
|
||||
|
||||
Reference in New Issue
Block a user