feat: IDEF0 visualization — ICOM boxes (control top, input left, output right, mechanism bottom)

This commit is contained in:
wasrusgen 2026-05-30 10:46:37 +03:00
parent 2abde49023
commit 3671ffe318

View File

@ -55,6 +55,26 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.blk-pain::before{content:'•';position:absolute;left:3px;color:#f59e0b} .blk-pain::before{content:'•';position:absolute;left:3px;color:#f59e0b}
.blk-tobe{font-size:13px;color:var(--primary);line-height:1.5;background:var(--light);padding:8px 10px;border-radius:8px} .blk-tobe{font-size:13px;color:var(--primary);line-height:1.5;background:var(--light);padding:8px 10px;border-radius:8px}
.empty{text-align:center;color:#cbd5e1;padding:60px 20px;font-size:14px} .empty{text-align:center;color:#cbd5e1;padding:60px 20px;font-size:14px}
/* IDEF0 box */
.idef-lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin:18px 0 10px}
.idef{margin-bottom:14px}
.idef-c{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-bottom:5px;padding-bottom:5px;border-bottom:2px dashed #cbd5e1}
.idef-mid{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:stretch}
.idef-i,.idef-o{display:flex;flex-direction:column;gap:4px;justify-content:center;max-width:130px}
.idef-fn{background:var(--white);border:2px solid var(--primary);border-radius:11px;padding:12px 10px;text-align:center;font-size:13px;font-weight:700;color:var(--dark);display:flex;flex-direction:column;gap:4px;justify-content:center;min-height:60px}
.idef-fn b{font-size:11px;color:var(--primary);font-family:'Montserrat'}
.idef-fn i{font-style:normal;font-size:11px;font-weight:700}
.idef-m{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-top:5px;padding-top:5px;border-top:2px dashed #cbd5e1}
.ar{font-size:10.5px;line-height:1.3;padding:3px 7px;border-radius:6px;background:#F1F5F9;color:#475569;display:inline-block}
.ar em{font-style:normal;color:#94a3b8;font-size:9px;margin-left:2px}
.idef-c .ar{background:#FEF3C7;color:#92400E}
.idef-c .ar.miss,.idef-c .ar.nomiss{background:#FEF2F2;color:#DC2626;border:1px dashed #FECACA}
.idef-i .ar{background:#EFF6FF;color:#1E40AF}
.idef-o .ar{background:#ECFDF5;color:#047857}
.idef-o .ar.dead{background:#FEF2F2;color:#DC2626}
.idef-m .ar.mech{background:#F5F3FF;color:#6D28D9}
.ar.dim{background:transparent;color:#cbd5e1}
.idef-iss{margin:-8px 0 14px;padding-left:4px}
@media(max-width:860px){.model-col{position:fixed;inset:56px 0 0 0;width:100%;max-width:none;z-index:50}.chat-col{border:none}} @media(max-width:860px){.model-col{position:fixed;inset:56px 0 0 0;width:100%;max-width:none;z-index:50}.chat-col{border:none}}
</style> </style>
</head> </head>
@ -140,46 +160,57 @@ async function sendMsg(){
} }
function pctColor(p){return p>=70?"#047857":p>=45?"#F59E0B":"#EF4444"} function pctColor(p){return p>=70?"#047857":p>=45?"#F59E0B":"#EF4444"}
function pctBg(p){return p>=70?"#ECFDF5":p>=45?"#FEF3C7":"#FEF2F2"} const SEV={critical:["#DC2626","#FEF2F2","КРИТ"],high:["#92400E","#FEF3C7","ВЫС"],medium:["#1E40AF","#EFF6FF","СРЕД"]};
const SEV={critical:["#DC2626","#FEF2F2","КРИТИЧНО"],high:["#92400E","#FEF3C7","ВЫСОКИЙ"],medium:["#1E40AF","#EFF6FF","СРЕДНИЙ"]}; const MTYPE={human:"чел",equipment:"обор",software:"ПО",none:"—"};
function idefBox(fn, ctrls, ins, outs, mechs, opts={}){
// Классический IDEF0 блок: Control сверху, Input слева, Output справа, Mechanism снизу
const C = (ctrls&&ctrls.length) ? ctrls.map(c=>`<span class="ar ${c.exists===false?'miss':''}">${esc(c.name)}</span>`).join("") : `<span class="ar nomiss">нет управления</span>`;
const I = (ins&&ins.length) ? ins.map(a=>`<span class="ar">${esc(a.name)}${a.source&&!/клиент|внеш|анна|подпис/i.test(a.source)?'<em>←'+esc(a.source)+'</em>':''}</span>`).join("") : `<span class="ar dim"></span>`;
const O = (outs&&outs.length) ? outs.map(a=>`<span class="ar ${a.target==='НИКУДА'?'dead':''}">${esc(a.name)}${a.target&&a.target!=='НИКУДА'&&!/клиент|внеш|анна/i.test(a.target)?'<em>→'+esc(a.target)+'</em>':a.target==='НИКУДА'?' ⊘':''}</span>`).join("") : `<span class="ar dim"></span>`;
const M = (mechs&&mechs.length) ? mechs.map(m=>`<span class="ar mech">${esc(m.name)}<em>${MTYPE[m.type]||''}</em></span>`).join("") : `<span class="ar dim"></span>`;
return `<div class="idef">
<div class="idef-c">${C}</div>
<div class="idef-mid">
<div class="idef-i">${I}</div>
<div class="idef-fn">${opts.id?`<b>${opts.id}</b>`:''}${esc(fn)}${opts.pct!=null?`<i style="color:${pctColor(opts.pct)}">${opts.pct}%</i>`:''}</div>
<div class="idef-o">${O}</div>
</div>
<div class="idef-m">${M}</div>
</div>`;
}
function renderModel(m){ function renderModel(m){
const col = document.getElementById("modelCol"); const col = document.getElementById("modelCol");
col.classList.add("show"); col.classList.add("show");
let html = `<div class="mc-head">Операционная карта</div><div class="mc-sum">${esc(m.client_summary)}</div>`; let html = `<div class="mc-head">Модель бизнеса · IDEF0</div><div class="mc-sum">${esc(m.client_summary)}</div>`;
if(m.business_pattern){ if(m.business_pattern){
html += `<div style="background:#0F0F1A;color:#fff;border-radius:11px;padding:12px 14px;margin-bottom:16px;font-size:12px;line-height:1.5"><div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#6EE7B7;margin-bottom:4px">Паттерн бизнеса</div>${esc(m.business_pattern)}</div>`; html += `<div style="background:#0F0F1A;color:#fff;border-radius:11px;padding:12px 14px;margin-bottom:18px;font-size:12px;line-height:1.5"><div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#6EE7B7;margin-bottom:4px">Паттерн бизнеса</div>${esc(m.business_pattern)}</div>`;
} }
// A-0 контекст
// Узлы if(m.context){
html += `<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin-bottom:8px">Узлы бизнеса · ${m.nodes.length}</div>`; html += `<div class="idef-lbl">A-0 · Контекстная диаграмма</div>`;
m.nodes.forEach(n=>{ const c=m.context;
html += `<div class="blk"> html += idefBox(c.function, c.controls, c.inputs, c.outputs, c.mechanisms, {id:"A0 "});
<div class="blk-top"><div class="blk-title">${esc(n.name)}</div> }
<div class="blk-pct" style="color:${pctColor(n.completeness)};background:${pctBg(n.completeness)}">${n.completeness}%</div></div> // A0 декомпозиция
<div style="font-size:12px;color:#6366F1;font-weight:600;margin-bottom:8px">${esc(n.actor)}</div> if(m.activities&&m.activities.length){
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px"> html += `<div class="idef-lbl">A0 · Декомпозиция · ${m.activities.length} функций</div>`;
<div style="background:#EFF6FF;border-radius:8px;padding:7px 9px"><div class="blk-lbl" style="color:#3B82F6">→ Вход</div><div style="font-size:12px;color:#374151;line-height:1.4">${esc(n.input)}</div></div> m.activities.forEach(a=>{
<div style="background:#ECFDF5;border-radius:8px;padding:7px 9px"><div class="blk-lbl" style="color:#047857">Выход →</div><div style="font-size:12px;color:#374151;line-height:1.4">${esc(n.output)}</div></div> html += idefBox(a.function, a.controls, a.inputs, a.outputs, a.mechanisms, {id:a.node_id+" ", pct:a.completeness});
</div> if(a.issues&&a.issues.length){html+=`<div class="idef-iss">`;a.issues.forEach(p=>html+=`<div class="blk-pain">${esc(p)}</div>`);html+=`</div>`}
<div class="blk-row"><div class="blk-lbl">Нормы</div><div style="font-size:12px;color:${n.norms.toLowerCase().includes('нет норм')?'#DC2626':'#374151'};line-height:1.4">${esc(n.norms)}</div></div> });
<div class="blk-row"><div class="blk-lbl">Ресурсы</div><div style="font-size:12px;color:#374151;line-height:1.4">${esc(n.resources)}</div></div>`; }
if(n.connections&&n.connections.length) html+=`<div class="blk-row"><div class="blk-lbl">Связи</div><div style="font-size:11px;color:#6b7280">${n.connections.map(esc).join(' · ')}</div></div>`; // Анализ стрелок
if(n.issues&&n.issues.length){html+=`<div class="blk-row"><div class="blk-lbl" style="color:#92400e">Проблемы</div>`;n.issues.forEach(p=>html+=`<div class="blk-pain">${esc(p)}</div>`);html+=`</div>`} if(m.arrow_issues&&m.arrow_issues.length){
html += `</div>`; html += `<div class="idef-lbl">Анализ стрелок · ${m.arrow_issues.length} разрывов</div>`;
}); m.arrow_issues.forEach(g=>{
// Паттерны проблем
if(m.gaps&&m.gaps.length){
html += `<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin:18px 0 8px">Найденные разрывы · ${m.gaps.length}</div>`;
m.gaps.forEach(g=>{
const s=SEV[g.severity]||SEV.medium; const s=SEV[g.severity]||SEV.medium;
html+=`<div class="blk" style="border-left:3px solid ${s[0]}"> html+=`<div class="blk" style="border-left:3px solid ${s[0]};padding:11px 14px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:5px"><span style="font-size:9px;font-weight:800;color:${s[0]};background:${s[1]};padding:2px 7px;border-radius:5px">${s[2]}</span><span class="blk-title">${esc(g.title)}</span></div> <div style="display:flex;align-items:center;gap:7px;margin-bottom:4px"><span style="font-size:9px;font-weight:800;color:${s[0]};background:${s[1]};padding:2px 6px;border-radius:5px">${s[2]}</span><span style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)}</span><span class="blk-title" style="font-size:13px">${esc(g.title)}</span></div>
<div style="font-size:12px;color:#6b7280;line-height:1.45">${esc(g.description)}</div></div>`; <div style="font-size:12px;color:#6b7280;line-height:1.45">${esc(g.description)}</div></div>`;
}); });
} }
if(m.missing_info&&m.missing_info.length){ if(m.missing_info&&m.missing_info.length){
html+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div class="blk-title" style="margin-bottom:8px">Елена уточнит ещё</div>`; html+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div class="blk-title" style="margin-bottom:8px">Елена уточнит ещё</div>`;
m.missing_info.forEach(q=>html+=`<div class="blk-pain">${esc(q)}</div>`);html+=`</div>`; m.missing_info.forEach(q=>html+=`<div class="blk-pain">${esc(q)}</div>`);html+=`</div>`;