686 lines
27 KiB
HTML
686 lines
27 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>场景 Skill 生成器</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f3efe4;
|
||
--panel: rgba(255, 252, 247, 0.88);
|
||
--panel-strong: #fffaf2;
|
||
--text: #1f2329;
|
||
--muted: #636b74;
|
||
--line: rgba(31, 35, 41, 0.12);
|
||
--accent: #0f766e;
|
||
--accent-strong: #115e59;
|
||
--warn: #b45309;
|
||
--error: #b42318;
|
||
--success: #166534;
|
||
--shadow: 0 24px 60px rgba(34, 42, 53, 0.14);
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(circle at top left, rgba(15, 118, 110, 0.18), transparent 32%),
|
||
radial-gradient(circle at right, rgba(180, 83, 9, 0.14), transparent 28%),
|
||
linear-gradient(160deg, #f5f0e6 0%, #eef5f4 56%, #f7f3eb 100%);
|
||
padding: 24px;
|
||
}
|
||
.shell {
|
||
width: min(1040px, 100%);
|
||
margin: 0 auto;
|
||
background: var(--panel);
|
||
backdrop-filter: blur(14px);
|
||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||
border-radius: 28px;
|
||
box-shadow: var(--shadow);
|
||
overflow: hidden;
|
||
}
|
||
.hero {
|
||
padding: 28px 28px 18px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: linear-gradient(135deg, rgba(255, 250, 242, 0.96), rgba(237, 246, 243, 0.92));
|
||
}
|
||
.hero h1 { margin: 0; font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.05; letter-spacing: 0.02em; }
|
||
.hero p { margin: 10px 0 0; max-width: 60ch; color: var(--muted); line-height: 1.6; }
|
||
.content { display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 0; }
|
||
.sidebar, .stream-panel { padding: 24px; }
|
||
.sidebar { border-right: 1px solid var(--line); background: rgba(255, 255, 255, 0.38); }
|
||
.section-label { margin: 0 0 14px; font-size: 0.83rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
|
||
.field { margin-bottom: 16px; }
|
||
.field label { display: block; margin-bottom: 6px; font-size: 0.92rem; color: var(--muted); }
|
||
.input-row { display: flex; gap: 8px; }
|
||
.input-row input { flex: 1; }
|
||
.input-row .browse-btn { width: auto; min-width: 60px; padding: 10px 14px; font-size: 0.85rem; }
|
||
input, button {
|
||
width: 100%;
|
||
border: 1px solid var(--line);
|
||
border-radius: 16px;
|
||
font: inherit;
|
||
}
|
||
input {
|
||
background: rgba(255, 255, 255, 0.92);
|
||
color: var(--text);
|
||
padding: 12px 14px;
|
||
outline: none;
|
||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||
}
|
||
input:focus { border-color: rgba(15, 118, 110, 0.5); box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12); }
|
||
select {
|
||
width: 100%;
|
||
border: 1px solid var(--line);
|
||
border-radius: 16px;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
color: var(--text);
|
||
padding: 12px 14px;
|
||
font: inherit;
|
||
outline: none;
|
||
cursor: pointer;
|
||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||
}
|
||
select:focus { border-color: rgba(15, 118, 110, 0.5); box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12); }
|
||
button { border: 0; padding: 12px 14px; font-weight: 700; cursor: pointer; transition: transform 140ms ease, opacity 140ms ease; }
|
||
button:hover:not(:disabled) { transform: translateY(-1px); }
|
||
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
||
.primary-btn { background: linear-gradient(135deg, var(--accent), var(--accent-strong)); color: #f6fffd; box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18); }
|
||
.ghost-btn { background: rgba(255, 255, 255, 0.9); color: var(--text); border: 1px solid var(--line); }
|
||
.status-card { display: grid; gap: 8px; padding: 14px; border-radius: 20px; background: var(--panel-strong); border: 1px solid var(--line); margin-bottom: 16px; }
|
||
.state-chip { display: inline-flex; align-items: center; width: fit-content; padding: 5px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 700; background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
||
.state-chip[data-state="ready"] { background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
||
.state-chip[data-state="analyzing"] { background: rgba(180, 83, 9, 0.12); color: var(--warn); }
|
||
.state-chip[data-state="generating"] { background: rgba(15, 118, 110, 0.12); color: var(--accent); }
|
||
.state-chip[data-state="complete"] { background: rgba(22, 101, 52, 0.12); color: var(--success); }
|
||
.state-chip[data-state="error"] { background: rgba(180, 35, 24, 0.12); color: var(--error); }
|
||
.validation { min-height: 1.4em; margin: 8px 0 12px; color: var(--error); font-size: 0.92rem; }
|
||
.stream-panel { display: grid; grid-template-rows: auto minmax(320px, 1fr); gap: 18px; }
|
||
.stream-head { display: flex; justify-content: space-between; align-items: end; gap: 16px; }
|
||
.stream-head h2 { margin: 0; font-size: 1.35rem; }
|
||
.stream-head p { margin: 6px 0 0; color: var(--muted); font-size: 0.94rem; }
|
||
.stream { display: grid; gap: 12px; align-content: start; min-height: 320px; max-height: 70vh; overflow: auto; padding: 4px; }
|
||
.empty-state { padding: 22px; border-radius: 20px; background: rgba(255, 255, 255, 0.52); border: 1px dashed rgba(31, 35, 41, 0.16); color: var(--muted); line-height: 1.6; }
|
||
.row { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: start; padding: 14px 16px; border-radius: 18px; background: rgba(255, 255, 255, 0.76); border: 1px solid rgba(31, 35, 41, 0.08); animation: rise 180ms ease; }
|
||
.row-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 76px; padding: 6px 10px; border-radius: 999px; font-size: 0.76rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; background: rgba(99, 107, 116, 0.14); color: var(--muted); }
|
||
.row.status .row-badge { background: rgba(15, 118, 110, 0.14); color: var(--accent-strong); }
|
||
.row.log .row-badge { background: rgba(57, 91, 163, 0.14); color: #315aa2; }
|
||
.row.complete .row-badge { background: rgba(22, 101, 52, 0.14); color: var(--success); }
|
||
.row.error .row-badge { background: rgba(180, 35, 24, 0.14); color: var(--error); }
|
||
.row-text { margin: 0; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
||
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||
.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>
|
||
<div class="shell">
|
||
<div class="hero">
|
||
<h1>场景 Skill 生成器</h1>
|
||
<p>选择场景目录,配置参数,一键生成 skill 包。</p>
|
||
</div>
|
||
<div class="content">
|
||
<div class="sidebar">
|
||
<p class="section-label">Status</p>
|
||
<div class="status-card">
|
||
<span id="stateChip" class="state-chip" data-state="ready">就绪</span>
|
||
<span id="statusText">请选择场景目录</span>
|
||
</div>
|
||
<p class="section-label">Source</p>
|
||
<div class="field">
|
||
<label>场景目录路径</label>
|
||
<div class="input-row">
|
||
<input id="sourceDir" placeholder="点击浏览选择目录..." readonly />
|
||
<button id="browseSourceDir" class="ghost-btn browse-btn">浏览</button>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label for="sceneId">scene-id</label>
|
||
<input id="sceneId" placeholder="例如:tq-lineloss-report" />
|
||
</div>
|
||
<div class="field">
|
||
<label for="sceneName">scene-name</label>
|
||
<input id="sceneName" placeholder="例如:台区线损报表" />
|
||
</div>
|
||
<div class="field">
|
||
<label for="sceneKind">场景类型</label>
|
||
<select id="sceneKind">
|
||
<option value="report_collection" selected>报表收集类</option>
|
||
<option value="monitoring">监测类</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="targetUrl">目标 URL (可选)</label>
|
||
<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">
|
||
<label>输出根路径</label>
|
||
<div class="input-row">
|
||
<input id="settingOutputRoot" placeholder="点击浏览选择目录..." readonly />
|
||
<button id="browseOutputRoot" class="ghost-btn browse-btn">浏览</button>
|
||
</div>
|
||
</div>
|
||
<div id="validationText" class="validation"></div>
|
||
<button id="generateBtn" class="primary-btn" disabled>生成 Skill</button>
|
||
</div>
|
||
<div class="stream-panel">
|
||
<div class="stream-head">
|
||
<div>
|
||
<p class="section-label">Generation Log</p>
|
||
<h2>实时日志</h2>
|
||
<p>显示生成过程的完整输出</p>
|
||
</div>
|
||
</div>
|
||
<div id="messageStream" class="stream">
|
||
<div class="empty-state" id="emptyState">选择场景目录并点击"生成 Skill"开始。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const SERVER_URL = "http://127.0.0.1:3210";
|
||
const els = {
|
||
sourceDir: document.getElementById("sourceDir"),
|
||
sceneId: document.getElementById("sceneId"),
|
||
sceneName: document.getElementById("sceneName"),
|
||
sceneKind: document.getElementById("sceneKind"),
|
||
targetUrl: document.getElementById("targetUrl"),
|
||
browseSourceDir: document.getElementById("browseSourceDir"),
|
||
browseOutputRoot: document.getElementById("browseOutputRoot"),
|
||
settingOutputRoot: document.getElementById("settingOutputRoot"),
|
||
generateBtn: document.getElementById("generateBtn"),
|
||
validationText: document.getElementById("validationText"),
|
||
stateChip: document.getElementById("stateChip"),
|
||
statusText: document.getElementById("statusText"),
|
||
messageStream: document.getElementById("messageStream"),
|
||
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;
|
||
els.stateChip.dataset.state = state;
|
||
els.statusText.textContent = text;
|
||
}
|
||
|
||
function setValidation(msg) { els.validationText.textContent = msg; }
|
||
|
||
function updateGenerateBtn() {
|
||
const ready = els.sourceDir.value.trim() && els.sceneId.value.trim() && els.sceneName.value.trim() && defaultsLoaded;
|
||
els.generateBtn.disabled = !ready;
|
||
}
|
||
|
||
function appendRow(kind, text) {
|
||
if (els.emptyState) { els.emptyState.remove(); els.emptyState = null; }
|
||
const row = document.createElement("div");
|
||
row.className = "row " + kind;
|
||
const badge = document.createElement("span");
|
||
badge.className = "row-badge";
|
||
badge.textContent = kind;
|
||
const content = document.createElement("p");
|
||
content.className = "row-text";
|
||
content.textContent = text;
|
||
row.appendChild(badge);
|
||
row.appendChild(content);
|
||
els.messageStream.appendChild(row);
|
||
els.messageStream.scrollTop = els.messageStream.scrollHeight;
|
||
}
|
||
|
||
async function selectFolder(defaultPath) {
|
||
try {
|
||
const res = await fetch(`${SERVER_URL}/select-folder`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ defaultPath }),
|
||
});
|
||
const data = await res.json();
|
||
return data.path;
|
||
} catch (err) {
|
||
console.error("Failed to select folder:", err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function selectFile(defaultPath, filter) {
|
||
try {
|
||
const res = await fetch(`${SERVER_URL}/select-file`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ defaultPath, filter }),
|
||
});
|
||
const data = await res.json();
|
||
return data.path;
|
||
} catch (err) {
|
||
console.error("Failed to select file:", err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function loadDefaults() {
|
||
try {
|
||
const res = await fetch(`${SERVER_URL}/health`);
|
||
if (!res.ok) throw new Error("health check failed");
|
||
defaultsLoaded = true;
|
||
const health = await res.json();
|
||
if (health.projectRoot) {
|
||
const root = health.projectRoot.replace(/\\/g, "/");
|
||
els.settingOutputRoot.value = root + "/examples/generated_scene_platform";
|
||
}
|
||
updateGenerateBtn();
|
||
} catch (err) {
|
||
console.error("Failed to load defaults:", err);
|
||
setState("error", "无法连接服务器");
|
||
appendRow("error", `服务器连接失败: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
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();
|
||
const sceneName = els.sceneName.value.trim();
|
||
const sceneKind = els.sceneKind.value;
|
||
const targetUrl = els.targetUrl.value.trim();
|
||
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
|
||
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(requestBody),
|
||
});
|
||
if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); }
|
||
|
||
const reader = res.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = "";
|
||
let lastEvent = "";
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split("\n");
|
||
buffer = lines.pop() || "";
|
||
for (const line of lines) {
|
||
if (line.startsWith("event:")) { lastEvent = line.slice(6).trim(); }
|
||
else if (line.startsWith("data:") && line.trim()) {
|
||
const dataStr = line.slice(5).trim();
|
||
if (!dataStr) continue;
|
||
try {
|
||
const data = JSON.parse(dataStr);
|
||
switch (lastEvent) {
|
||
case "status": appendRow("status", data.message); break;
|
||
case "log": appendRow("log", data.message); break;
|
||
case "complete":
|
||
if (data.success) { setState("complete", "生成完成"); appendRow("complete", `生成完成: ${data.skillRoot || ""}`); }
|
||
else { setState("error", "生成失败"); appendRow("error", data.message || "生成失败"); }
|
||
break;
|
||
case "error": setState("error", "生成失败"); appendRow("error", data.message); break;
|
||
default: appendRow("log", JSON.stringify(data));
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
setState("error", "生成失败");
|
||
appendRow("error", err.message);
|
||
} finally {
|
||
els.generateBtn.disabled = false;
|
||
updateGenerateBtn();
|
||
}
|
||
}
|
||
|
||
// Browse buttons
|
||
els.browseSourceDir.addEventListener("click", async () => {
|
||
const path = await selectFolder(els.sourceDir.value || null);
|
||
if (path) {
|
||
els.sourceDir.value = path;
|
||
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, "/"));
|
||
}
|
||
});
|
||
|
||
els.browseOutputRoot.addEventListener("click", async () => {
|
||
const path = await selectFolder(els.settingOutputRoot.value || null);
|
||
if (path) {
|
||
els.settingOutputRoot.value = path;
|
||
updateGenerateBtn();
|
||
}
|
||
});
|
||
|
||
els.generateBtn.addEventListener("click", generate);
|
||
els.sceneId.addEventListener("input", updateGenerateBtn);
|
||
els.sceneName.addEventListener("input", updateGenerateBtn);
|
||
|
||
loadDefaults();
|
||
</script>
|
||
</body>
|
||
</html>
|