wasrusgen1-crm/docs/elena_live.html

238 lines
15 KiB
HTML
Raw 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>Елена — живой кабинет · @wasrusgen1 | КОНСАЛТИНГ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--primary:#047857;--dark:#064E3B;--mid:#10B981;--light:#ECFDF5;--bg:#F5F6F8;--white:#fff;--border:#E5E7EB;--text:#1A1A2E;--muted:#6B7280}
html,body{height:100%}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column;height:100vh}
.hdr{height:56px;background:var(--dark);display:flex;align-items:center;padding:0 20px;gap:12px;flex-shrink:0}
.hdr-ic{width:32px;height:32px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;color:#fff;font-size:17px}
.hdr-t{font-family:'Montserrat';font-weight:700;font-size:15px;color:#fff}
.hdr-t em{color:rgba(255,255,255,.4);font-style:normal;margin:0 5px}
.hdr-elena{margin-left:auto;display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.85);font-size:13px;font-weight:600}
.hdr-dot{width:8px;height:8px;border-radius:50%;background:var(--mid);box-shadow:0 0 6px var(--mid)}
.wrap{flex:1;display:flex;overflow:hidden}
.chat-col{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid var(--border)}
.chat{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:14px}
.msg{display:flex;gap:10px;max-width:80%}
.msg.user{align-self:flex-end;flex-direction:row-reverse}
.av{width:32px;height:32px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;font-size:13px;color:#fff}
.av.elena{background:var(--primary)}
.av.user{background:#6366F1}
.bubble{padding:11px 15px;border-radius:14px;font-size:14px;line-height:1.55;white-space:pre-wrap}
.bubble.elena{background:var(--white);border:1px solid var(--border);border-top-left-radius:4px}
.bubble.user{background:var(--primary);color:#fff;border-top-right-radius:4px}
.bubble strong{font-weight:700}
.typing{display:flex;gap:4px;padding:14px 16px;background:var(--white);border:1px solid var(--border);border-radius:14px;border-top-left-radius:4px;width:fit-content}
.typing span{width:7px;height:7px;border-radius:50%;background:#cbd5e1;animation:b 1.2s infinite}
.typing span:nth-child(2){animation-delay:.2s}.typing span:nth-child(3){animation-delay:.4s}
@keyframes b{0%,80%,100%{transform:scale(.7);opacity:.4}40%{transform:scale(1);opacity:1}}
.inbar{padding:14px 20px;border-top:1px solid var(--border);background:var(--white);display:flex;gap:10px;align-items:flex-end;flex-shrink:0}
.inp{flex:1;border:1.5px solid var(--border);border-radius:12px;padding:11px 15px;font-size:14px;font-family:'Inter';resize:none;outline:none;max-height:120px;line-height:1.5}
.inp:focus{border-color:var(--mid);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
.send{width:44px;height:44px;border-radius:11px;background:var(--primary);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.send:hover{background:var(--dark)}.send:disabled{opacity:.4;cursor:default}
.build-btn{margin:0 20px 14px;padding:13px;border-radius:12px;background:linear-gradient(135deg,var(--dark),var(--primary));color:#fff;border:none;cursor:pointer;font-family:'Inter';font-weight:700;font-size:14px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:8px}
.build-btn:hover{opacity:.92}.build-btn:disabled{opacity:.5;cursor:default}
.model-col{width:42%;max-width:560px;overflow-y:auto;padding:24px;background:#fafbfc;display:none}
.model-col.show{display:block}
.mc-head{font-family:'Montserrat';font-weight:800;font-size:18px;color:var(--dark);margin-bottom:6px}
.mc-sum{font-size:13px;color:var(--muted);line-height:1.5;margin-bottom:18px;padding-bottom:18px;border-bottom:1px solid var(--border)}
.blk{background:var(--white);border:1px solid var(--border);border-radius:13px;padding:16px;margin-bottom:12px}
.blk-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.blk-title{font-size:14px;font-weight:700;color:var(--text)}
.blk-pct{font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px}
.blk-row{margin-bottom:9px}
.blk-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin-bottom:3px}
.blk-asis{font-size:13px;color:#374151;line-height:1.5}
.blk-pain{font-size:12px;color:#92400e;line-height:1.45;padding-left:14px;position:relative;margin-bottom:3px}
.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}
.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}}
</style>
</head>
<body>
<div class="hdr">
<div class="hdr-ic">@</div>
<div class="hdr-t">wasrusgen1 <em>/</em> КОНСАЛТИНГ</div>
<div class="hdr-elena"><span class="hdr-dot"></span>Елена · онлайн</div>
</div>
<div class="wrap">
<div class="chat-col">
<div class="chat" id="chat"></div>
<button class="build-btn" id="buildBtn" onclick="buildModel()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Построить мою бизнес-модель →
</button>
<div class="inbar">
<textarea class="inp" id="inp" rows="1" placeholder="Напишите Елене..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
<button class="send" id="sendBtn" onclick="sendMsg()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<div class="model-col" id="modelCol">
<div class="empty">Бизнес-модель появится здесь после анализа</div>
</div>
</div>
<script>
const API = "https://claude-83-172-150-111.sslip.io/elena";
let token = localStorage.getItem("elena_token");
const chat = document.getElementById("chat");
const inp = document.getElementById("inp");
function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
function addMsg(role, text){
const m = document.createElement("div");
m.className = "msg " + (role==="user"?"user":"elena");
m.innerHTML = `<div class="av ${role==='user'?'user':'elena'}">${role==='user'?'Я':'Е'}</div><div class="bubble ${role==='user'?'user':'elena'}">${fmt(text)}</div>`;
chat.appendChild(m);
chat.scrollTop = chat.scrollHeight;
}
function showTyping(){
const t = document.createElement("div");
t.className="msg elena";t.id="typing";
t.innerHTML=`<div class="av elena">Е</div><div class="typing"><span></span><span></span><span></span></div>`;
chat.appendChild(t);chat.scrollTop=chat.scrollHeight;
}
function hideTyping(){const t=document.getElementById("typing");if(t)t.remove()}
async function init(){
if(token){
const r = await fetch(`${API}/api/project/${token}`);
if(r.ok){
const d = await r.json();
d.messages.forEach(m=>addMsg(m.role==="user"?"user":"elena", m.content));
if(d.model) renderModel(d.model);
return;
}
}
const r = await fetch(`${API}/api/project/new`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});
const d = await r.json();
token = d.token; localStorage.setItem("elena_token", token);
addMsg("elena", d.greeting);
}
async function sendMsg(){
const text = inp.value.trim();
if(!text) return;
inp.value=""; inp.style.height="auto";
addMsg("user", text);
document.getElementById("sendBtn").disabled=true;
showTyping();
try{
const r = await fetch(`${API}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:text})});
const d = await r.json();
hideTyping();
addMsg("elena", d.reply || ("Ошибка: "+(d.error||"нет ответа")));
}catch(e){hideTyping();addMsg("elena","Ошибка соединения: "+e.message)}
document.getElementById("sendBtn").disabled=false;
}
function pctColor(p){return p>=70?"#047857":p>=45?"#F59E0B":"#EF4444"}
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){
const col = document.getElementById("modelCol");
col.classList.add("show");
let html = `<div class="mc-head">Модель бизнеса · IDEF0</div><div class="mc-sum">${esc(m.client_summary)}</div>`;
if(m.business_pattern){
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 class="idef-lbl">A-0 · Контекстная диаграмма</div>`;
const c=m.context;
html += idefBox(c.function, c.controls, c.inputs, c.outputs, c.mechanisms, {id:"A0 "});
}
// A0 декомпозиция
if(m.activities&&m.activities.length){
html += `<div class="idef-lbl">A0 · Декомпозиция · ${m.activities.length} функций</div>`;
m.activities.forEach(a=>{
html += idefBox(a.function, a.controls, a.inputs, a.outputs, a.mechanisms, {id:a.node_id+" ", pct:a.completeness});
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>`}
});
}
// Анализ стрелок
if(m.arrow_issues&&m.arrow_issues.length){
html += `<div class="idef-lbl">Анализ стрелок · ${m.arrow_issues.length} разрывов</div>`;
m.arrow_issues.forEach(g=>{
const s=SEV[g.severity]||SEV.medium;
html+=`<div class="blk" style="border-left:3px solid ${s[0]};padding:11px 14px">
<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>`;
});
}
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>`;
m.missing_info.forEach(q=>html+=`<div class="blk-pain">${esc(q)}</div>`);html+=`</div>`;
}
col.innerHTML = html;
}
async function buildModel(){
const btn = document.getElementById("buildBtn");
btn.disabled=true; btn.innerHTML="Елена анализирует ваш бизнес... ⏳";
try{
const r = await fetch(`${API}/api/build-model`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token})});
const d = await r.json();
if(d.model){renderModel(d.model);btn.innerHTML="✓ Модель построена · обновить";}
else{btn.innerHTML="Ошибка: "+(d.error||"?");}
}catch(e){btn.innerHTML="Ошибка: "+e.message}
btn.disabled=false;
}
inp.addEventListener("input",()=>{inp.style.height="auto";inp.style.height=Math.min(inp.scrollHeight,120)+"px"});
init();
</script>
</body>
</html>