862e6b7b25
Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
12 KiB
HTML
232 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>ВКУС — целевой sequence (CJM)</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f4f4f5; --card: #fff; --border: #d4d4d8; --text: #18181b; --muted: #71717a;
|
||
--primary: #1d4ed8; --note-bg: #f8fafc;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font-family: "Segoe UI", system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
||
header { background: var(--card); border-bottom: 1px solid var(--border); padding: 14px 20px; }
|
||
header h1 { margin: 0 0 4px; font-size: 1.15rem; }
|
||
header p { margin: 0; color: var(--muted); font-size: 0.85rem; }
|
||
main { padding: 16px 20px 40px; max-width: 1400px; margin: 0 auto; }
|
||
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
|
||
.panel h2 { margin: 0 0 4px; font-size: 1.05rem; }
|
||
.panel .hint { margin: 0 0 10px; color: var(--muted); font-size: 0.82rem; }
|
||
.note { background: var(--note-bg); border: 1px solid var(--border); border-left: 3px solid var(--primary); padding: 10px 12px; margin-bottom: 12px; font-size: 0.82rem; line-height: 1.5; border-radius: 0 6px 6px 0; }
|
||
.note strong { color: var(--primary); }
|
||
.legend { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 12px; font-size: 0.76rem; color: var(--muted); }
|
||
.legend i { display: inline-block; width: 28px; height: 0; vertical-align: middle; margin-right: 4px; border-top: 2px solid #18181b; }
|
||
.legend .ret i { border-top: 2px dashed #52525b; }
|
||
.legend .fb i { border-top: 2px solid #ea580c; }
|
||
.svg-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: 6px; background: #fff; }
|
||
svg { display: block; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>ВКУС — целевой sequence по клиентскому пути</h1>
|
||
<p>Целевая архитектура (CJM) · HTML + SVG · без Mermaid</p>
|
||
</header>
|
||
<main>
|
||
<div class="panel">
|
||
<h2>Цель: CJM</h2>
|
||
<p class="hint">docs/cjm_request_sequence.md · время сверху вниз · return (пунктир) от отвечающего к вызывающему</p>
|
||
<div class="note">
|
||
<strong>SystemGate</strong> (проверка ИТ-системы) — модуль в слое «Намерения».
|
||
По тексту запроса определяет, относится ли вопрос к поддерживаемой системе
|
||
(Meridium 4, ЛИМС НИОКР, 1С, StarLims и др.).
|
||
Если система не определена или не поддерживается — поиск не запускается,
|
||
пользователю возвращается просьба уточнить контекст (<code>blocked</code>).
|
||
</div>
|
||
<div class="legend">
|
||
<span><i></i> вызов (сплошная →)</span>
|
||
<span class="ret"><i></i> ответ return (пунктир →)</span>
|
||
<span class="fb"><i></i> fallback (оранжевый)</span>
|
||
</div>
|
||
<div class="svg-scroll" id="seq-target"></div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
(function () {
|
||
"use strict";
|
||
|
||
var STYLES = {
|
||
actor: { fill: "#fef9c3", stroke: "#ca8a04" },
|
||
core: { fill: "#eff6ff", stroke: "#3b82f6" },
|
||
integ: { fill: "#f0fdf4", stroke: "#16a34a" },
|
||
ext: { fill: "#fef3c7", stroke: "#d97706" },
|
||
fragAlt:{ fill: "rgba(255,247,237,0.92)", stroke: "#ea580c" },
|
||
fragOpt:{ fill: "rgba(248,250,252,0.92)", stroke: "#64748b" },
|
||
};
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
|
||
function fragRect(f, msgs, xs, y0, rowH) {
|
||
var minP = 999, maxP = -1;
|
||
var extra = f.participants || [];
|
||
extra.forEach(function (p) { minP = Math.min(minP, p); maxP = Math.max(maxP, p); });
|
||
for (var i = f.start; i <= f.end; i++) {
|
||
if (!msgs[i]) continue;
|
||
minP = Math.min(minP, msgs[i].from, msgs[i].to);
|
||
maxP = Math.max(maxP, msgs[i].from, msgs[i].to);
|
||
}
|
||
if (minP > maxP) return null;
|
||
var margin = 58;
|
||
return {
|
||
x: xs[minP] - margin,
|
||
y: y0 + f.start * rowH - 16,
|
||
w: xs[maxP] - xs[minP] + margin * 2,
|
||
h: (f.end - f.start + 1) * rowH + 8,
|
||
};
|
||
}
|
||
|
||
function renderSequence(el, cfg) {
|
||
var uid = cfg.id || "s";
|
||
var parts = cfg.participants;
|
||
var msgs = cfg.messages;
|
||
var colW = cfg.colW || 118;
|
||
var left = 28;
|
||
var top = 62;
|
||
var rowH = cfg.rowH || 38;
|
||
var headH = 50;
|
||
var w = left + 30 + colW * parts.length + 40;
|
||
var h = top + headH + msgs.length * rowH + 50;
|
||
|
||
var xs = parts.map(function (_, i) { return left + 50 + i * colW; });
|
||
var y0 = top + headH;
|
||
|
||
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">';
|
||
svg += '<defs>';
|
||
svg += '<marker id="' + uid + '-ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#18181b"/></marker>';
|
||
svg += '<marker id="' + uid + '-fb" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#ea580c"/></marker>';
|
||
svg += '<marker id="' + uid + '-ret" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#52525b"/></marker>';
|
||
svg += '</defs>';
|
||
|
||
parts.forEach(function (p, i) {
|
||
var st = STYLES[p.kind] || STYLES.core;
|
||
var x = xs[i];
|
||
var bw = Math.min(colW - 8, p.kind === "actor" ? 76 : 100);
|
||
svg += '<rect x="' + (x - bw / 2) + '" y="10" width="' + bw + '" height="' + headH + '" rx="4" fill="' + st.fill + '" stroke="' + st.stroke + '" stroke-width="1.2"/>';
|
||
(p.label || "").split("\n").forEach(function (ln, li) {
|
||
svg += '<text x="' + x + '" y="' + (30 + li * 13) + '" text-anchor="middle" font-size="11" font-weight="600">' + esc(ln) + '</text>';
|
||
});
|
||
if (p.sub) {
|
||
svg += '<text x="' + x + '" y="' + (headH - 2) + '" text-anchor="middle" font-size="9" fill="#71717a">' + esc(p.sub) + '</text>';
|
||
}
|
||
svg += '<line x1="' + x + '" y1="' + (10 + headH) + '" x2="' + x + '" y2="' + (h - 16) + '" stroke="#d4d4d8" stroke-width="1" stroke-dasharray="5 4"/>';
|
||
});
|
||
|
||
if (cfg.fragments) {
|
||
cfg.fragments.forEach(function (f) {
|
||
var r = fragRect(f, msgs, xs, y0, rowH);
|
||
if (!r) return;
|
||
var st = f.kind === "opt" ? STYLES.fragOpt : STYLES.fragAlt;
|
||
svg += '<rect x="' + r.x + '" y="' + r.y + '" width="' + r.w + '" height="' + r.h + '" fill="' + st.fill + '" stroke="' + st.stroke + '" stroke-width="1.2" rx="5"/>';
|
||
svg += '<text x="' + (r.x + 8) + '" y="' + (r.y + 15) + '" font-size="10" font-weight="700" fill="' + st.stroke + '">' + esc(f.label) + '</text>';
|
||
});
|
||
}
|
||
|
||
msgs.forEach(function (m, idx) {
|
||
var y = y0 + idx * rowH + rowH / 2;
|
||
var fi = m.from, ti = m.to;
|
||
var xFrom = xs[fi], xTo = xs[ti];
|
||
var isRet = m.type === "return";
|
||
var isSelf = fi === ti;
|
||
var isFb = m.fallback;
|
||
var col = isFb ? "#ea580c" : (isRet ? "#52525b" : "#18181b");
|
||
var mk = uid + (isFb ? "-fb" : (isRet ? "-ret" : "-ar"));
|
||
var dash = isRet ? ' stroke-dasharray="7 5"' : "";
|
||
|
||
if (isSelf) {
|
||
var sx = xFrom + 10;
|
||
svg += '<path d="M' + sx + ' ' + y + ' h32 v16 h-32" fill="none" stroke="' + col + '" stroke-width="1.4" marker-end="url(#' + mk + ')"/>';
|
||
svg += '<text x="' + (sx + 38) + '" y="' + (y + 4) + '" font-size="10" fill="#3f3f46">' + esc(m.label) + '</text>';
|
||
} else {
|
||
svg += '<line x1="' + xFrom + '" y1="' + y + '" x2="' + xTo + '" y2="' + y + '" stroke="' + col + '" stroke-width="1.4"' + dash + ' marker-end="url(#' + mk + ')"/>';
|
||
var lx = (xFrom + xTo) / 2;
|
||
var anchor = "middle";
|
||
if (Math.abs(xTo - xFrom) < 80) { lx = Math.max(xFrom, xTo) + 6; anchor = "start"; }
|
||
svg += '<text x="' + lx + '" y="' + (y - 6) + '" text-anchor="' + anchor + '" font-size="10" fill="#3f3f46">' + esc(m.label) + '</text>';
|
||
}
|
||
|
||
if (m.num) {
|
||
svg += '<circle cx="' + (left + 4) + '" cy="' + y + '" r="10" fill="#1d4ed8"/>';
|
||
svg += '<text x="' + (left + 4) + '" y="' + (y + 4) + '" text-anchor="middle" font-size="9" fill="#fff" font-weight="700">' + m.num + '</text>';
|
||
}
|
||
});
|
||
|
||
svg += '</svg>';
|
||
el.innerHTML = svg;
|
||
}
|
||
|
||
renderSequence(document.getElementById("seq-target"), {
|
||
id: "tgt",
|
||
colW: 110,
|
||
rowH: 40,
|
||
participants: [
|
||
{ label: "Сотрудник", kind: "actor" },
|
||
{ label: "Витрина", sub: "CAP-01", kind: "core" },
|
||
{ label: "Backend", sub: "оркестратор", kind: "core" },
|
||
{ label: "Намерения", sub: "CAP-02", kind: "core" },
|
||
{ label: "Знания", sub: "CAP-03", kind: "core" },
|
||
{ label: "API ИИ", sub: "интеграция", kind: "integ" },
|
||
{ label: "RAG", sub: "BERT", kind: "integ" },
|
||
{ label: "LLM", kind: "integ" },
|
||
{ label: "Creatio", sub: "ITSM", kind: "ext" },
|
||
],
|
||
fragments: [
|
||
{ label: "alt [skip_rag] — обход поиска", start: 4, end: 6 },
|
||
{ label: "alt [gate blocked] — ИТ-система не определена", start: 7, end: 9 },
|
||
{ label: "opt [корпоративный RAG]", start: 12, end: 15, kind: "opt" },
|
||
{ label: "alt [RAG недоступен] → локальный индекс", start: 15, end: 15 },
|
||
{ label: "opt [усиление LLM]", start: 16, end: 19, kind: "opt" },
|
||
{ label: "alt [LLM недоступен] → шаблон", start: 20, end: 20 },
|
||
{ label: "alt [Помогло] — deflection", start: 24, end: 26 },
|
||
{ label: "alt [эскалация] — заявка в ITSM", start: 27, end: 28 },
|
||
],
|
||
messages: [
|
||
{ num: 1, from: 0, to: 1, label: "Описывает задачу" },
|
||
{ num: 2, from: 1, to: 2, label: "POST /api/analyze" },
|
||
{ num: 3, from: 2, to: 3, label: "route_dialog()" },
|
||
{ num: 4, from: 2, to: 2, label: "сессия, trace-id" },
|
||
{ num: 5, from: 3, to: 2, label: "skip_rag — короткий ответ", type: "return", fallback: true },
|
||
{ num: 6, from: 2, to: 1, label: "ответ без поиска", type: "return" },
|
||
{ num: 7, from: 1, to: 0, label: "реплика в чате", type: "return" },
|
||
{ num: 8, from: 2, to: 3, label: "check_system_gate()" },
|
||
{ num: 9, from: 3, to: 2, label: "blocked — уточните систему", type: "return", fallback: true },
|
||
{ num: 10, from: 2, to: 1, label: "сообщение пользователю", type: "return" },
|
||
{ num: 11, from: 2, to: 4, label: "search + history" },
|
||
{ num: 12, from: 4, to: 4, label: "локальный индекс" },
|
||
{ num: 13, from: 4, to: 5, label: "retrieval (JWT)" },
|
||
{ num: 14, from: 5, to: 6, label: "эмбеддинги + поиск" },
|
||
{ num: 15, from: 6, to: 5, label: "top-k фрагменты", type: "return" },
|
||
{ num: 16, from: 4, to: 4, label: "fallback: локальный поиск", fallback: true },
|
||
{ num: 17, from: 4, to: 5, label: "chat/completions" },
|
||
{ num: 18, from: 5, to: 7, label: "генерация" },
|
||
{ num: 19, from: 7, to: 5, label: "текст + confidence", type: "return" },
|
||
{ num: 20, from: 4, to: 2, label: "fallback: шаблон", type: "return", fallback: true },
|
||
{ num: 21, from: 4, to: 2, label: "хиты, draft, confidence", type: "return" },
|
||
{ num: 22, from: 2, to: 1, label: "ответ, шаги, источники", type: "return" },
|
||
{ num: 23, from: 1, to: 0, label: "«Помогло» / «Не помогло»" },
|
||
{ num: 24, from: 0, to: 1, label: "Помогло" },
|
||
{ num: 25, from: 1, to: 2, label: "POST /api/feedback" },
|
||
{ num: 26, from: 2, to: 1, label: "deflection OK", type: "return" },
|
||
{ num: 27, from: 2, to: 8, label: "create_case()" },
|
||
{ num: 28, from: 8, to: 2, label: "ticket_id, SLA", type: "return" },
|
||
{ num: 29, from: 2, to: 1, label: "номер заявки", type: "return" },
|
||
],
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|