Files
claw/frontend/scene-generator/sg_scene_generator.html

1218 lines
40 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Scene Skill Generator</title>
<!-- 鍦烘櫙 Skill 鐢熸垚鍣? -->
<style>
:root {
--bg: #efe9dd;
--panel: rgba(255, 251, 246, 0.86);
--panel-strong: #fffaf2;
--text: #1f2329;
--muted: #5f6772;
--line: rgba(31, 35, 41, 0.12);
--accent: #0f766e;
--accent-strong: #115e59;
--warn: #b45309;
--error: #b42318;
--success: #166534;
--info: #315aa2;
--shadow: 0 24px 60px rgba(34, 42, 53, 0.14);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
padding: 24px;
color: var(--text);
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(15, 118, 110, 0.16), transparent 34%),
radial-gradient(circle at right, rgba(180, 83, 9, 0.12), transparent 28%),
linear-gradient(160deg, #f5f0e6 0%, #edf5f2 56%, #f7f3eb 100%);
}
.shell {
width: min(1180px, 100%);
margin: 0 auto;
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: 28px;
box-shadow: var(--shadow);
overflow: hidden;
backdrop-filter: blur(14px);
}
.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: 70ch;
color: var(--muted);
line-height: 1.6;
}
.content {
display: grid;
grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
}
.sidebar,
.main {
padding: 24px;
}
.sidebar {
border-right: 1px solid var(--line);
background: rgba(255, 255, 255, 0.34);
}
.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);
}
.hint {
margin-top: 6px;
font-size: 0.8rem;
color: var(--muted);
line-height: 1.5;
}
.input-row {
display: flex;
gap: 8px;
}
.input-row input {
flex: 1;
}
input,
select,
button {
width: 100%;
border-radius: 16px;
font: inherit;
}
input,
select {
border: 1px solid var(--line);
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,
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 {
opacity: 0.45;
cursor: not-allowed;
}
.browse-btn {
width: auto;
min-width: 74px;
padding-inline: 14px;
background: rgba(255, 255, 255, 0.9);
color: var(--text);
border: 1px solid var(--line);
}
.primary-btn {
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
color: #f6fffd;
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18);
}
.secondary-btn {
background: rgba(15, 118, 110, 0.1);
color: var(--accent);
border: 1px solid rgba(15, 118, 110, 0.26);
}
.status-card,
.readiness-card {
display: grid;
gap: 8px;
padding: 14px;
border-radius: 20px;
background: var(--panel-strong);
border: 1px solid var(--line);
margin-bottom: 16px;
}
.state-chip,
.readiness-chip,
.tag {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 5px 10px;
border-radius: 999px;
font-size: 0.84rem;
font-weight: 700;
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); }
.readiness-chip[data-level="A"] { background: rgba(22, 101, 52, 0.12); color: var(--success); }
.readiness-chip[data-level="B"] { background: rgba(180, 83, 9, 0.12); color: var(--warn); }
.readiness-chip[data-level="C"] { 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;
}
.btn-group {
display: flex;
gap: 8px;
}
.btn-group button {
flex: 1;
}
.main {
display: grid;
gap: 18px;
align-content: start;
background: rgba(255, 255, 255, 0.18);
}
.panel {
background: rgba(255, 255, 255, 0.6);
border-radius: 20px;
border: 1px solid var(--line);
overflow: hidden;
}
.panel-head {
padding: 16px 18px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.52);
}
.panel-head h2,
.panel-head h3 {
margin: 0;
font-size: 1.05rem;
}
.panel-head p {
margin: 6px 0 0;
color: var(--muted);
line-height: 1.5;
font-size: 0.9rem;
}
.panel-body {
padding: 18px;
}
.grid {
display: grid;
gap: 14px;
}
.grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.kv {
display: grid;
gap: 6px;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.7);
}
.kv .label {
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.kv .value {
font-size: 0.96rem;
line-height: 1.5;
word-break: break-word;
}
.banner {
padding: 12px 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.72);
line-height: 1.55;
}
.banner.warn {
border-color: rgba(180, 83, 9, 0.24);
background: rgba(180, 83, 9, 0.08);
}
.banner.error {
border-color: rgba(180, 35, 24, 0.24);
background: rgba(180, 35, 24, 0.08);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.list {
display: grid;
gap: 10px;
}
.list-item {
padding: 12px 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.74);
}
.list-item strong {
display: block;
margin-bottom: 4px;
}
.list-item .meta {
color: var(--muted);
font-size: 0.85rem;
line-height: 1.5;
}
.code {
margin: 0;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.04);
font-family: Consolas, Monaco, monospace;
font-size: 0.82rem;
line-height: 1.55;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
}
.stream {
display: grid;
gap: 12px;
min-height: 320px;
max-height: 46vh;
overflow: auto;
}
.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(49, 90, 162, 0.14); color: var(--info); }
.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;
}
.hidden {
display: none !important;
}
@keyframes rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 980px) {
body { padding: 16px; }
.content { grid-template-columns: 1fr; }
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); }
.grid.two { grid-template-columns: 1fr; }
.stream { max-height: none; }
}
</style>
</head>
<body>
<div class="shell">
<div class="hero">
<h1>Scene Skill Generator</h1>
<p>Analyze a frontend scene directory, inspect merged Scene IR preview, override the workflow archetype when needed, then generate with `scene-ir-json` instead of the old flat scene info payload.</p>
</div>
<div class="content">
<aside class="sidebar">
<p class="section-label">Status</p>
<div class="status-card">
<span id="stateChip" class="state-chip" data-state="ready">Ready</span>
<span id="statusText">Select a scene directory.</span>
</div>
<div id="readinessCard" class="readiness-card hidden">
<span id="readinessChip" class="readiness-chip" data-level="">-</span>
<div id="readinessSummary">No analysis yet.</div>
<div id="readinessConfidence" class="hint"></div>
</div>
<p class="section-label">Source</p>
<div class="field">
<label for="sourceDir">Scene directory</label>
<div class="input-row">
<input id="sourceDir" placeholder="Choose a scene directory..." readonly />
<button id="browseSourceDir" class="browse-btn" type="button">Browse</button>
</div>
</div>
<div class="field">
<label for="sceneId">scene-id</label>
<input id="sceneId" placeholder="example: tq-lineloss-report" />
</div>
<div class="field">
<label for="sceneName">scene-name</label>
<input id="sceneName" placeholder="example: TQ line loss report" />
</div>
<div class="field">
<label for="sceneKind">Scene kind</label>
<select id="sceneKind">
<option value="report_collection" selected>report_collection</option>
<option value="monitoring">monitoring</option>
</select>
</div>
<div class="field">
<label for="targetUrl">Target URL override</label>
<input id="targetUrl" placeholder="Optional business URL override" />
<div class="hint">If set, this is applied on top of the analyzed bootstrap preview before generation.</div>
</div>
<div class="field">
<label for="workflowArchetypeOverride">Workflow archetype override</label>
<select id="workflowArchetypeOverride">
<option value="">Use detected archetype</option>
<option value="single_request_table">single_request_table</option>
<option value="single_request_enrichment">single_request_enrichment</option>
<option value="multi_mode_request">multi_mode_request</option>
<option value="paginated_enrichment">paginated_enrichment</option>
<option value="page_state_eval">page_state_eval</option>
</select>
<div id="detectedArchetypeHint" class="hint">Detected archetype: -</div>
</div>
<div class="btn-group" style="margin-bottom: 16px;">
<button id="analyzeBtn" class="secondary-btn" type="button">Analyze Deep</button>
<button id="resetOverrideBtn" class="browse-btn" type="button">Reset</button>
</div>
<p class="section-label">Output</p>
<div class="field">
<label for="settingOutputRoot">Output root</label>
<div class="input-row">
<input id="settingOutputRoot" placeholder="Choose output root..." readonly />
<button id="browseOutputRoot" class="browse-btn" type="button">Browse</button>
</div>
</div>
<div id="validationText" class="validation"></div>
<button id="generateBtn" class="primary-btn" type="button" disabled>Generate Skill</button>
</aside>
<main class="main">
<section id="analysisPanel" class="panel hidden">
<div class="panel-head">
<h2>Analysis Preview</h2>
<p>Deterministic signals are merged with optional LLM semantics. Hard facts stay deterministic-first; conflicts appear as warnings.</p>
</div>
<div class="panel-body">
<div class="grid two">
<div class="kv">
<div class="label">Detected Scene ID</div>
<div id="previewSceneId" class="value">-</div>
</div>
<div class="kv">
<div class="label">Scene ID Source</div>
<div id="previewSceneIdSource" class="value">-</div>
</div>
<div class="kv">
<div class="label">Scene ID Validation</div>
<div id="previewSceneIdValidation" class="value">-</div>
</div>
<div class="kv">
<div class="label">Detected Archetype</div>
<div id="previewArchetype" class="value">-</div>
</div>
<div class="kv">
<div class="label">Overall Confidence</div>
<div id="previewConfidence" class="value">-</div>
</div>
<div class="kv">
<div class="label">Bootstrap Domain</div>
<div id="previewExpectedDomain" class="value">-</div>
</div>
<div class="kv">
<div class="label">Bootstrap Target URL</div>
<div id="previewTargetUrl" class="value">-</div>
</div>
</div>
<div id="previewWarnings" class="grid" style="margin-top: 14px;"></div>
<div class="grid two" style="margin-top: 14px;">
<div>
<p class="section-label">Endpoints</p>
<div id="previewEndpoints" class="list"></div>
</div>
<div>
<p class="section-label">Workflow Steps</p>
<div id="previewWorkflowSteps" class="list"></div>
</div>
</div>
<div class="grid two" style="margin-top: 14px;">
<div>
<p class="section-label">Params</p>
<div id="previewParams" class="list"></div>
</div>
<div>
<p class="section-label">Modes</p>
<div id="previewModes" class="list"></div>
</div>
</div>
<div class="grid two" style="margin-top: 14px;">
<div>
<p class="section-label">Evidence</p>
<div id="previewEvidence" class="list"></div>
</div>
<div>
<p class="section-label">Risks / Missing Pieces</p>
<div id="previewRisks" class="list"></div>
</div>
</div>
<div class="grid two" style="margin-top: 14px;">
<div>
<p class="section-label">Request Template</p>
<pre id="previewRequestTemplate" class="code">{}</pre>
</div>
<div>
<p class="section-label">Static Params</p>
<pre id="previewStaticParams" class="code">{}</pre>
</div>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<h3>Generation Log</h3>
<p>Streaming output from the generator. Completion also repeats readiness and archetype so the execution risk stays visible after generation.</p>
</div>
<div class="panel-body">
<div id="messageStream" class="stream">
<div id="emptyState" class="empty-state">Choose a scene directory, run deep analysis, then generate a skill package.</div>
</div>
</div>
</section>
</main>
</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"),
workflowArchetypeOverride: document.getElementById("workflowArchetypeOverride"),
detectedArchetypeHint: document.getElementById("detectedArchetypeHint"),
browseSourceDir: document.getElementById("browseSourceDir"),
browseOutputRoot: document.getElementById("browseOutputRoot"),
settingOutputRoot: document.getElementById("settingOutputRoot"),
analyzeBtn: document.getElementById("analyzeBtn"),
resetOverrideBtn: document.getElementById("resetOverrideBtn"),
generateBtn: document.getElementById("generateBtn"),
validationText: document.getElementById("validationText"),
stateChip: document.getElementById("stateChip"),
statusText: document.getElementById("statusText"),
readinessCard: document.getElementById("readinessCard"),
readinessChip: document.getElementById("readinessChip"),
readinessSummary: document.getElementById("readinessSummary"),
readinessConfidence: document.getElementById("readinessConfidence"),
analysisPanel: document.getElementById("analysisPanel"),
previewSceneId: document.getElementById("previewSceneId"),
previewSceneIdSource: document.getElementById("previewSceneIdSource"),
previewSceneIdValidation: document.getElementById("previewSceneIdValidation"),
previewArchetype: document.getElementById("previewArchetype"),
previewConfidence: document.getElementById("previewConfidence"),
previewExpectedDomain: document.getElementById("previewExpectedDomain"),
previewTargetUrl: document.getElementById("previewTargetUrl"),
previewWarnings: document.getElementById("previewWarnings"),
previewEndpoints: document.getElementById("previewEndpoints"),
previewWorkflowSteps: document.getElementById("previewWorkflowSteps"),
previewParams: document.getElementById("previewParams"),
previewModes: document.getElementById("previewModes"),
previewEvidence: document.getElementById("previewEvidence"),
previewRisks: document.getElementById("previewRisks"),
previewRequestTemplate: document.getElementById("previewRequestTemplate"),
previewStaticParams: document.getElementById("previewStaticParams"),
messageStream: document.getElementById("messageStream"),
emptyState: document.getElementById("emptyState")
};
let defaultsLoaded = false;
let currentSceneIr = null;
let detectedWorkflowArchetype = "";
function setState(state, text) {
els.stateChip.dataset.state = state;
els.stateChip.textContent = text;
els.statusText.textContent = text;
}
function setValidation(message) {
els.validationText.textContent = message || "";
}
function updateGenerateBtn() {
const ready = Boolean(
els.sourceDir.value.trim() &&
els.sceneId.value.trim() &&
els.sceneName.value.trim() &&
els.settingOutputRoot.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;
}
function createListItems(container, items, renderer, emptyText) {
container.innerHTML = "";
if (!items || !items.length) {
const item = document.createElement("div");
item.className = "list-item";
item.innerHTML = `<div class="meta">${escapeHtml(emptyText)}</div>`;
container.appendChild(item);
return;
}
for (const entry of items) {
const item = document.createElement("div");
item.className = "list-item";
item.innerHTML = renderer(entry);
container.appendChild(item);
}
}
function renderReadiness(readiness, confidence) {
if (!readiness || !readiness.level) {
els.readinessCard.classList.add("hidden");
return;
}
els.readinessCard.classList.remove("hidden");
els.readinessChip.dataset.level = readiness.level;
els.readinessChip.textContent = `Readiness ${readiness.level}`;
els.readinessSummary.textContent = readiness.notes && readiness.notes.length
? readiness.notes[0]
: "Analysis completed.";
const safeConfidence = typeof readiness.confidence === "number" && readiness.confidence > 0
? readiness.confidence
: confidence || 0;
els.readinessConfidence.textContent = `confidence ${(safeConfidence * 100).toFixed(0)}%`;
}
function renderAnalysis(sceneIr) {
currentSceneIr = sceneIr;
detectedWorkflowArchetype = sceneIr.workflowArchetype || "";
els.analysisPanel.classList.remove("hidden");
els.detectedArchetypeHint.textContent = `Detected archetype: ${detectedWorkflowArchetype || "-"}`;
if (!els.workflowArchetypeOverride.value) {
els.workflowArchetypeOverride.value = "";
}
els.previewSceneId.textContent = sceneIr.sceneId || "-";
els.previewSceneIdSource.textContent = sceneIr.sceneIdDiagnostics?.candidateSource || "-";
els.previewSceneIdValidation.textContent = sceneIr.sceneIdDiagnostics?.valid
? "valid"
: (sceneIr.sceneIdDiagnostics?.invalidReason || "invalid");
els.previewArchetype.textContent = sceneIr.workflowArchetype || "-";
els.previewConfidence.textContent = typeof sceneIr.confidence === "number"
? `${(sceneIr.confidence * 100).toFixed(0)}%`
: "-";
els.previewExpectedDomain.textContent = sceneIr.bootstrap?.expectedDomain || "-";
els.previewTargetUrl.textContent = sceneIr.bootstrap?.targetUrl || "-";
els.previewRequestTemplate.textContent = JSON.stringify(sceneIr.requestTemplate || {}, null, 2);
els.previewStaticParams.textContent = JSON.stringify(sceneIr.staticParams || {}, null, 2);
createWarnings(sceneIr);
createListItems(
els.previewEndpoints,
sceneIr.apiEndpoints || [],
(endpoint) => `
<strong>${escapeHtml(endpoint.name || "endpoint")}</strong>
<div class="meta">${escapeHtml(endpoint.method || "GET")} ${escapeHtml(endpoint.url || "")}</div>
<div class="meta">${escapeHtml(endpoint.contentType || "")}</div>
`,
"No endpoints detected."
);
createListItems(
els.previewWorkflowSteps,
sceneIr.workflowSteps || [],
(step) => `
<strong>${escapeHtml(step.type || "step")}</strong>
<div class="meta">${escapeHtml(step.description || "")}</div>
<div class="meta">${escapeHtml([step.entry, step.endpoint, step.expr].filter(Boolean).join(" | "))}</div>
`,
"No workflow steps detected."
);
createListItems(
els.previewParams,
sceneIr.params || [],
(param) => `
<strong>${escapeHtml(param.name || "param")}</strong>
<div class="meta">${escapeHtml(param.resolver || "")}${param.required ? " | required" : ""}</div>
`,
"No params inferred."
);
createListItems(
els.previewModes,
sceneIr.modes || [],
(mode) => `
<strong>${escapeHtml(mode.name || "mode")}</strong>
<div class="meta">${escapeHtml(mode.label || "")}</div>
<div class="meta">${escapeHtml(mode.condition ? `${mode.condition.field} ${mode.condition.operator} ${String(mode.condition.value)}` : "")}</div>
`,
"No explicit modes detected."
);
createListItems(
els.previewEvidence,
sceneIr.evidence || [],
(evidence) => `
<strong>${escapeHtml(evidence.kind || "evidence")}</strong>
<div class="meta">${escapeHtml(evidence.summary || "")}</div>
<div class="meta">${escapeHtml(evidence.source || "")} | ${(Number(evidence.confidence || 0) * 100).toFixed(0)}%</div>
`,
"No evidence attached."
);
const riskItems = [
...((sceneIr.readiness && sceneIr.readiness.gates) || []).map((gate) => ({
title: gate.passed ? "gate-pass" : "gate-fail",
text: `${gate.name}${gate.reason ? ` | ${gate.reason}` : ""}`
})),
...((sceneIr.readiness && sceneIr.readiness.risks) || []).map((text) => ({ title: "risk", text })),
...((sceneIr.readiness && sceneIr.readiness.missingPieces) || []).map((text) => ({ title: "missing", text })),
...((sceneIr.uncertainties) || []).map((text) => ({ title: "uncertainty", text }))
];
createListItems(
els.previewRisks,
riskItems,
(item) => `
<strong>${escapeHtml(item.title)}</strong>
<div class="meta">${escapeHtml(item.text)}</div>
`,
"No major risks surfaced."
);
renderReadiness(sceneIr.readiness, sceneIr.confidence);
updateGenerateBtn();
}
function createWarnings(sceneIr) {
els.previewWarnings.innerHTML = "";
const warnings = (sceneIr.analysisMeta && sceneIr.analysisMeta.warnings) || [];
if (!warnings.length && sceneIr.readiness && sceneIr.readiness.level === "A") {
const banner = document.createElement("div");
banner.className = "banner";
banner.textContent = "No merge conflicts or readiness blockers were reported.";
els.previewWarnings.appendChild(banner);
return;
}
const allWarnings = warnings.length ? warnings : ["Review readiness risks before generation."];
for (const warning of allWarnings) {
const banner = document.createElement("div");
banner.className = `banner ${sceneIr.readiness && sceneIr.readiness.level === "C" ? "error" : "warn"}`;
banner.textContent = warning;
els.previewWarnings.appendChild(banner);
}
}
async function selectFolder(defaultPath) {
try {
const response = await fetch(`${SERVER_URL}/select-folder`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ defaultPath })
});
const data = await response.json();
return data.path || null;
} catch (error) {
console.error(error);
return null;
}
}
async function loadDefaults() {
try {
const response = await fetch(`${SERVER_URL}/health`);
if (!response.ok) throw new Error("health check failed");
const health = await response.json();
defaultsLoaded = true;
if (health.projectRoot) {
const root = health.projectRoot.replace(/\\/g, "/");
els.settingOutputRoot.value = `${root}/examples/generated_scene_platform`;
}
updateGenerateBtn();
} catch (error) {
setState("error", "Server unavailable");
appendRow("error", `Health check failed: ${error.message}`);
}
}
async function analyzeSourceDir(sourceDir) {
if (!sourceDir) return;
setState("analyzing", "Analyzing scene naming...");
appendRow("status", "Running basic scene-id / scene-name analysis...");
try {
const response = await fetch(`${SERVER_URL}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceDir })
});
const data = await response.json();
if (!response.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", `Basic analysis completed: ${data.sceneId || "-"} / ${data.sceneName || "-"}`);
} catch (error) {
appendRow("error", `Basic analysis failed: ${error.message}`);
} finally {
setState("ready", "Ready");
updateGenerateBtn();
}
}
async function analyzeDeep() {
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
if (!sourceDir) {
setValidation("Select a scene directory first.");
return;
}
setValidation("");
setState("analyzing", "Running deep analysis...");
appendRow("status", "Running deterministic + optional LLM deep analysis...");
try {
const response = await fetch(`${SERVER_URL}/analyze-deep`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceDir })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Deep analysis failed");
if (data.sceneId) els.sceneId.value = data.sceneId;
if (data.sceneName) els.sceneName.value = data.sceneName;
if (data.sceneKind) els.sceneKind.value = data.sceneKind;
if (data.bootstrap && data.bootstrap.targetUrl && !els.targetUrl.value.trim()) {
els.targetUrl.value = data.bootstrap.targetUrl;
}
renderAnalysis(data);
appendRow(
"status",
`Deep analysis completed: archetype=${data.workflowArchetype || "-"}, readiness=${data.readiness?.level || "-"}, endpoints=${(data.apiEndpoints || []).length}`
);
} catch (error) {
setState("error", "Analysis failed");
appendRow("error", `Deep analysis failed: ${error.message}`);
} finally {
if (els.stateChip.dataset.state !== "error") {
setState("ready", "Ready");
}
updateGenerateBtn();
}
}
function buildFallbackSceneIr() {
const overrideArchetype = els.workflowArchetypeOverride.value.trim();
const targetUrl = els.targetUrl.value.trim();
return {
sceneId: els.sceneId.value.trim() || "scene",
sceneName: els.sceneName.value.trim() || "Generated Scene",
sceneKind: els.sceneKind.value || "report_collection",
workflowArchetype: overrideArchetype || "single_request_table",
bootstrap: {
expectedDomain: targetUrl ? extractHostname(targetUrl) : "",
targetUrl: targetUrl || "",
requiresTargetPage: true,
pageTitleKeywords: [],
source: targetUrl ? "manual_override" : "manual"
},
params: [],
modes: [],
defaultMode: null,
modeSwitchField: null,
workflowSteps: [],
requestTemplate: {},
responsePath: "",
normalizeRules: null,
artifactContract: {
type: "report-artifact",
successStatus: ["ok", "partial", "empty"],
failureStatus: ["blocked", "error"]
},
validationHints: {
requiresTargetPage: true,
runtimeCompatible: true,
manualCompletionRequired: true,
missingPieces: ["analysis_missing"]
},
evidence: [],
readiness: {
level: "C",
confidence: 0.2,
risks: ["Generated without deep analysis preview."],
missingPieces: ["analysis_preview"],
notes: ["Run deep analysis before internal-network trial."]
},
apiEndpoints: [],
staticParams: {},
columnDefs: [],
confidence: 0.2,
uncertainties: ["No deep analysis preview available."]
};
}
function buildSceneIrForGeneration() {
const base = currentSceneIr
? JSON.parse(JSON.stringify(currentSceneIr))
: buildFallbackSceneIr();
base.sceneId = els.sceneId.value.trim() || base.sceneId;
base.sceneName = els.sceneName.value.trim() || base.sceneName;
base.sceneKind = els.sceneKind.value || base.sceneKind || "report_collection";
const overrideArchetype = els.workflowArchetypeOverride.value.trim();
if (overrideArchetype) {
base.workflowArchetype = overrideArchetype;
base.analysisMeta = base.analysisMeta || {};
base.analysisMeta.override = {
field: "workflowArchetype",
detected: detectedWorkflowArchetype || null,
applied: overrideArchetype
};
}
const manualTargetUrl = els.targetUrl.value.trim();
if (manualTargetUrl) {
base.bootstrap = base.bootstrap || {};
base.bootstrap.targetUrl = manualTargetUrl;
if (!base.bootstrap.expectedDomain) {
base.bootstrap.expectedDomain = extractHostname(manualTargetUrl);
}
base.bootstrap.source = "manual_override";
}
return base;
}
function extractHostname(rawUrl) {
try {
return new URL(rawUrl).hostname;
} catch (_) {
return "";
}
}
async function generate() {
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
const sceneId = els.sceneId.value.trim();
const sceneName = els.sceneName.value.trim();
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
if (!sourceDir || !sceneId || !sceneName || !outputRoot) {
setValidation("sourceDir, sceneId, sceneName, and outputRoot are required.");
return;
}
const sceneIr = buildSceneIrForGeneration();
setValidation("");
setState("generating", "Generating skill...");
els.generateBtn.disabled = true;
appendRow(
"status",
`Generating with scene-ir-json. archetype=${sceneIr.workflowArchetype || "-"} readiness=${sceneIr.readiness?.level || "-"}`
);
try {
const response = await fetch(`${SERVER_URL}/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourceDir,
sceneId,
sceneName,
sceneKind: els.sceneKind.value || null,
targetUrl: els.targetUrl.value.trim() || null,
outputRoot,
sceneIrJson: sceneIr
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Generation failed");
}
const reader = response.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();
continue;
}
if (!line.startsWith("data:")) {
continue;
}
const dataText = line.slice(5).trim();
if (!dataText) continue;
try {
const data = JSON.parse(dataText);
handleSseEvent(lastEvent, data);
} catch (_) {
}
}
}
} catch (error) {
setState("error", "Generation failed");
appendRow("error", error.message);
} finally {
if (els.stateChip.dataset.state !== "complete" && els.stateChip.dataset.state !== "error") {
setState("ready", "Ready");
}
updateGenerateBtn();
}
}
function handleSseEvent(eventName, data) {
switch (eventName) {
case "status":
appendRow("status", data.message || "");
break;
case "log":
if (data.message) appendRow("log", data.message);
break;
case "error":
setState("error", "Generation failed");
appendRow("error", data.message || "Generation failed");
break;
case "complete": {
if (data.success) {
setState("complete", "Generation complete");
const summaryParts = [`Generated: ${data.skillRoot || "(unknown output)"}`];
if (data.workflowArchetype) summaryParts.push(`archetype=${data.workflowArchetype}`);
if (data.readiness && data.readiness.level) summaryParts.push(`readiness=${data.readiness.level}`);
if (typeof data.confidence === "number" && data.confidence > 0) {
summaryParts.push(`confidence=${(data.confidence * 100).toFixed(0)}%`);
}
appendRow("complete", summaryParts.join(" | "));
if (data.readiness) {
renderReadiness(data.readiness, data.confidence || 0);
}
} else {
setState("error", "Generation failed");
appendRow("error", data.message || "Generation failed");
}
break;
}
default:
appendRow("log", JSON.stringify(data));
}
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
els.browseSourceDir.addEventListener("click", async () => {
const selected = await selectFolder(els.sourceDir.value || "");
if (!selected) return;
els.sourceDir.value = selected;
const folderName = selected.replace(/\\/g, "/").split("/").pop();
if (folderName && !els.sceneId.value.trim()) {
els.sceneId.value = folderName;
}
currentSceneIr = null;
detectedWorkflowArchetype = "";
els.analysisPanel.classList.add("hidden");
els.readinessCard.classList.add("hidden");
await analyzeSourceDir(selected.replace(/\\/g, "/"));
updateGenerateBtn();
});
els.browseOutputRoot.addEventListener("click", async () => {
const selected = await selectFolder(els.settingOutputRoot.value || "");
if (!selected) return;
els.settingOutputRoot.value = selected;
updateGenerateBtn();
});
els.analyzeBtn.addEventListener("click", analyzeDeep);
els.resetOverrideBtn.addEventListener("click", () => {
els.workflowArchetypeOverride.value = "";
});
els.generateBtn.addEventListener("click", generate);
els.sceneId.addEventListener("input", updateGenerateBtn);
els.sceneName.addEventListener("input", updateGenerateBtn);
els.settingOutputRoot.addEventListener("input", updateGenerateBtn);
loadDefaults();
</script>
</body>
</html>