mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 15:44:45 +00:00
293 lines
18 KiB
HTML
293 lines
18 KiB
HTML
<!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:14px;color:rgba(255,255,255,.6);display:flex;align-items:center;gap:9px;letter-spacing:-.2px}
|
||
.hdr-sep{width:1.5px;height:15px;background:rgba(255,255,255,.25);flex-shrink:0}
|
||
.hdr-t b{font-weight:800;color:#fff}
|
||
.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}
|
||
/* Microphone */
|
||
.mic{width:44px;height:44px;border-radius:11px;background:var(--light);border:1.5px solid rgba(4,120,87,.2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--primary);transition:all .15s;position:relative}
|
||
.mic:hover{background:#D1FAE5;border-color:var(--primary)}
|
||
.mic.rec{background:#FEF2F2;border-color:#EF4444;color:#EF4444}
|
||
.mic.rec::after{content:'';position:absolute;inset:-4px;border-radius:14px;border:2px solid #EF4444;animation:micPulse 1.3s ease-out infinite;pointer-events:none}
|
||
@keyframes micPulse{0%{transform:scale(1);opacity:.7}100%{transform:scale(1.25);opacity:0}}
|
||
.mic-hint{position:absolute;bottom:56px;right:0;background:var(--ink);color:#fff;font-size:12px;padding:6px 12px;border-radius:8px;white-space:nowrap;display:none;z-index:20}
|
||
.mic-hint.show{display:block}
|
||
.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<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></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="mic" id="micBtn" onclick="toggleMic()" title="Голосовой ввод">
|
||
<div class="mic-hint" id="micHint">Слушаю... говорите</div>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
|
||
</button>
|
||
<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,"&").replace(/</g,"<").replace(/>/g,">")}
|
||
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
|
||
|
||
// ── Голосовой ввод (Web Speech API, русский) ──────────
|
||
let recog=null, recording=false;
|
||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||
if(SR){
|
||
recog = new SR();
|
||
recog.lang = "ru-RU";
|
||
recog.continuous = true;
|
||
recog.interimResults = true;
|
||
let baseText = "";
|
||
recog.onresult = e=>{
|
||
let fin="", interim="";
|
||
for(let i=e.resultIndex;i<e.results.length;i++){
|
||
const t=e.results[i][0].transcript;
|
||
if(e.results[i].isFinal) fin+=t; else interim+=t;
|
||
}
|
||
if(fin) baseText += fin;
|
||
inp.value = (baseText + interim).trim();
|
||
inp.style.height="auto"; inp.style.height=Math.min(inp.scrollHeight,120)+"px";
|
||
};
|
||
recog.onstart = ()=>{ baseText = inp.value ? inp.value+" " : ""; };
|
||
recog.onend = ()=>{ if(recording){ try{recog.start()}catch(e){} } };
|
||
recog.onerror = e=>{ if(e.error==="not-allowed"){ alert("Разрешите доступ к микрофону в браузере"); stopMic(); } };
|
||
}
|
||
function toggleMic(){
|
||
if(!recog){ alert("Голосовой ввод не поддерживается этим браузером. Используйте Chrome."); return; }
|
||
recording ? stopMic() : startMic();
|
||
}
|
||
function startMic(){
|
||
recording=true;
|
||
document.getElementById("micBtn").classList.add("rec");
|
||
document.getElementById("micHint").classList.add("show");
|
||
try{ recog.start() }catch(e){}
|
||
}
|
||
function stopMic(){
|
||
recording=false;
|
||
document.getElementById("micBtn").classList.remove("rec");
|
||
document.getElementById("micHint").classList.remove("show");
|
||
try{ recog.stop() }catch(e){}
|
||
inp.focus();
|
||
}
|
||
|
||
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(){
|
||
if(recording) stopMic();
|
||
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>
|