Files

232 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
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>