From 7289cc5779512ea10528ceea3006ff038f363fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E7=82=8E?= <635735027@qq.com> Date: Fri, 17 Apr 2026 10:37:50 +0800 Subject: [PATCH] feat(ui): add deep extraction preview panel with API/column/static-params display --- .../scene-generator/sg_scene_generator.html | 334 ++++++++++++++++-- 1 file changed, 312 insertions(+), 22 deletions(-) diff --git a/frontend/scene-generator/sg_scene_generator.html b/frontend/scene-generator/sg_scene_generator.html index 37d4e1d..279d333 100644 --- a/frontend/scene-generator/sg_scene_generator.html +++ b/frontend/scene-generator/sg_scene_generator.html @@ -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); + } @@ -157,6 +253,67 @@

场景要访问的目标页面地址,留空则使用自动提取的域名

+ +
+ +
+ + + +

Settings

@@ -166,13 +323,6 @@
-
- -
- - -
-
@@ -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 => + `
${escapeHtml(ep)}
` + ).join(""); + } else { + apiCount.textContent = "0"; + apiList.innerHTML = '
'; + } + + // 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 => + `
${escapeHtml(col)}
` + ).join(""); + } else { + colCount.textContent = "0"; + colList.innerHTML = '
'; + } + + // 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, """); + } + + 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);