1218 lines
40 KiB
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
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>
|