720 lines
35 KiB
HTML
720 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Redator de Demandas — protótipo</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--ink:#0f2338;
|
||
--ink-2:#3c5470;
|
||
--ink-3:#7a8ca1;
|
||
--canvas:#e7eaef;
|
||
--panel:#ffffff;
|
||
--paper:#fdfdfa;
|
||
--line:#d6dbe3;
|
||
--line-2:#eceff4;
|
||
--teal:#0b8fa8;
|
||
--blue:#1f6fd0;
|
||
--amber:#a8701a;
|
||
--amber-bg:#fdf4e3;
|
||
--rose:#9e2f42;
|
||
--rose-bg:#fbeef0;
|
||
--green:#1f7a52;
|
||
--green-bg:#eaf4ee;
|
||
--r:3px;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%}
|
||
body{
|
||
background:var(--canvas);
|
||
color:var(--ink);
|
||
font-family:'Inter Tight',-apple-system,BlinkMacSystemFont,sans-serif;
|
||
font-size:14px;
|
||
line-height:1.5;
|
||
-webkit-font-smoothing:antialiased;
|
||
overflow:hidden;
|
||
}
|
||
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
||
:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
|
||
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||
|
||
/* ---------- shell ---------- */
|
||
.app{display:grid;grid-template-rows:auto 1fr;height:100vh}
|
||
|
||
.topbar{
|
||
display:flex;align-items:center;gap:14px;
|
||
padding:0 20px;height:52px;
|
||
background:var(--ink);color:#fff;
|
||
border-bottom:1px solid #06172a;
|
||
}
|
||
.logo{display:flex;align-items:center;gap:9px;font-weight:600;letter-spacing:-.01em}
|
||
.logo svg{flex:none}
|
||
.topbar .sep{width:1px;height:20px;background:rgba(255,255,255,.18)}
|
||
.topbar .ctx{font-size:12.5px;color:#9fb4ca}
|
||
.topbar .ctx b{color:#dce8f3;font-weight:500}
|
||
.topbar .right{margin-left:auto;display:flex;align-items:center;gap:10px}
|
||
.pill{
|
||
font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
|
||
padding:4px 9px;border-radius:2px;background:rgba(255,255,255,.1);color:#bcd3e8;
|
||
}
|
||
.btn-reset{font-size:12.5px;color:#9fb4ca;padding:5px 10px;border:1px solid rgba(255,255,255,.2);border-radius:var(--r)}
|
||
.btn-reset:hover{color:#fff;border-color:rgba(255,255,255,.45)}
|
||
|
||
.main{display:grid;grid-template-columns:1fr 1fr;overflow:hidden}
|
||
|
||
/* ---------- coluna esquerda: conversa ---------- */
|
||
.left{display:flex;flex-direction:column;background:var(--canvas);border-right:1px solid var(--line);overflow:hidden}
|
||
.left-head{
|
||
padding:13px 26px;border-bottom:1px solid var(--line);
|
||
display:flex;align-items:baseline;gap:10px;background:var(--canvas);
|
||
}
|
||
.eyebrow{font-size:10.5px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3)}
|
||
.left-head .step{margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--ink-3)}
|
||
|
||
.stream{flex:1;overflow-y:auto;padding:26px 26px 8px;scroll-behavior:smooth}
|
||
.stream::-webkit-scrollbar{width:9px}
|
||
.stream::-webkit-scrollbar-thumb{background:#c3cad4;border-radius:9px;border:3px solid var(--canvas)}
|
||
|
||
.msg{margin-bottom:22px;animation:rise .34s cubic-bezier(.2,.7,.3,1) both}
|
||
@keyframes rise{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
|
||
|
||
.who{
|
||
font-size:10.5px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
|
||
color:var(--ink-3);margin-bottom:7px;display:flex;align-items:center;gap:7px;
|
||
}
|
||
.who i{width:5px;height:5px;border-radius:50%;background:var(--teal);font-style:normal}
|
||
.msg.user .who i{background:var(--ink-3)}
|
||
|
||
.bubble{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:15px 17px;font-size:14px;line-height:1.62}
|
||
.msg.user .bubble{background:transparent;border-style:dashed;border-color:#c3cad4;color:var(--ink-2)}
|
||
.bubble p+p{margin-top:10px}
|
||
.bubble strong{font-weight:600}
|
||
.bubble .quiet{color:var(--ink-2)}
|
||
|
||
/* fonte da base de conhecimento */
|
||
.sources{margin-top:13px;padding-top:12px;border-top:1px solid var(--line-2)}
|
||
.sources-h{font-size:10.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-bottom:9px;display:flex;align-items:center;gap:6px}
|
||
.src{
|
||
display:flex;gap:10px;padding:8px 10px;border-radius:2px;background:#f4f6f9;
|
||
border-left:2px solid var(--teal);margin-bottom:6px;
|
||
}
|
||
.src:last-child{margin-bottom:0}
|
||
.src .id{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;color:var(--teal);flex:none;padding-top:1px}
|
||
.src .txt{font-size:12.5px;line-height:1.45;color:var(--ink-2)}
|
||
.src .txt b{color:var(--ink);font-weight:600}
|
||
|
||
/* alerta */
|
||
.flag{margin-top:13px;padding:11px 13px;background:var(--amber-bg);border-left:2px solid var(--amber);font-size:12.8px;line-height:1.5;color:#6b4a12}
|
||
.flag b{color:var(--amber)}
|
||
|
||
/* opções */
|
||
.opts{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px}
|
||
.opt{
|
||
border:1px solid var(--line);background:var(--panel);border-radius:var(--r);
|
||
padding:9px 14px;font-size:13.2px;font-weight:500;color:var(--ink);
|
||
transition:.16s;text-align:left;
|
||
}
|
||
.opt:hover{border-color:var(--blue);background:#f5f9ff;transform:translateY(-1px)}
|
||
.opt small{display:block;font-size:11.5px;font-weight:400;color:var(--ink-3);margin-top:2px}
|
||
.opt.wide{width:100%}
|
||
|
||
.typing{display:flex;gap:4px;padding:6px 0}
|
||
.typing span{width:5px;height:5px;border-radius:50%;background:var(--ink-3);animation:blip 1.1s infinite}
|
||
.typing span:nth-child(2){animation-delay:.16s}
|
||
.typing span:nth-child(3){animation-delay:.32s}
|
||
@keyframes blip{0%,60%,100%{opacity:.25}30%{opacity:1}}
|
||
|
||
/* composer */
|
||
.composer{border-top:1px solid var(--line);padding:14px 26px 18px;background:var(--canvas)}
|
||
.composer textarea{
|
||
width:100%;min-height:88px;resize:none;padding:13px 15px;font-family:inherit;font-size:13.5px;line-height:1.55;
|
||
border:1px solid var(--line);border-radius:var(--r);background:var(--panel);color:var(--ink);
|
||
}
|
||
.composer textarea::placeholder{color:#9aa8b8}
|
||
.composer textarea:focus{border-color:var(--blue);outline:none}
|
||
.composer-row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
||
.btn{
|
||
background:var(--ink);color:#fff;padding:9px 18px;border-radius:var(--r);
|
||
font-size:13.2px;font-weight:600;transition:.16s;
|
||
}
|
||
.btn:hover{background:#1c3d5e}
|
||
.btn:disabled{opacity:.35;cursor:not-allowed}
|
||
.btn.ghost{background:none;color:var(--ink-2);border:1px solid var(--line)}
|
||
.btn.ghost:hover{background:var(--panel);color:var(--ink)}
|
||
.hint{font-size:11.8px;color:var(--ink-3);margin-left:auto}
|
||
|
||
/* ---------- coluna direita: documento ---------- */
|
||
.right{display:flex;flex-direction:column;background:var(--panel);overflow:hidden}
|
||
.tabs{display:flex;border-bottom:1px solid var(--line);padding:0 26px;background:var(--panel);flex:none}
|
||
.tab{
|
||
padding:15px 0;margin-right:26px;font-size:12.5px;font-weight:600;color:var(--ink-3);
|
||
border-bottom:2px solid transparent;margin-bottom:-1px;letter-spacing:.01em;
|
||
}
|
||
.tab.on{color:var(--ink);border-bottom-color:var(--ink)}
|
||
.tab .n{font-family:'JetBrains Mono',monospace;font-size:10.5px;background:var(--line-2);color:var(--ink-2);padding:1px 5px;border-radius:2px;margin-left:6px}
|
||
|
||
.doc-wrap{flex:1;overflow-y:auto;background:#f1f3f6;padding:22px}
|
||
.doc-wrap::-webkit-scrollbar{width:9px}
|
||
.doc-wrap::-webkit-scrollbar-thumb{background:#c3cad4;border-radius:9px;border:3px solid #f1f3f6}
|
||
|
||
.doc{
|
||
background:var(--paper);border:1px solid var(--line);
|
||
padding:34px 38px 40px;max-width:660px;margin:0 auto;
|
||
font-family:'Newsreader',Georgia,serif;
|
||
}
|
||
.doc-head{border-bottom:2px solid var(--ink);padding-bottom:14px;margin-bottom:20px}
|
||
.doc-kicker{font-family:'Inter Tight',sans-serif;font-size:10.5px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--teal);margin-bottom:7px}
|
||
.doc h1{font-size:25px;font-weight:600;line-height:1.2;letter-spacing:-.01em}
|
||
.doc h1 .ph{color:#c0c8d2;font-style:italic;font-weight:400}
|
||
|
||
.doc h2{
|
||
font-family:'Inter Tight',sans-serif;font-size:11px;font-weight:700;letter-spacing:.11em;
|
||
text-transform:uppercase;color:var(--ink-3);margin:26px 0 11px;
|
||
padding-bottom:5px;border-bottom:1px solid var(--line-2);
|
||
}
|
||
.doc p{font-size:15px;line-height:1.62;margin-bottom:10px;color:#1c3category}
|
||
.doc p{color:#1b3049}
|
||
|
||
/* campos */
|
||
.fields{display:grid;grid-template-columns:auto 1fr;gap:0}
|
||
.fields dt{
|
||
font-family:'Inter Tight',sans-serif;font-size:11.5px;font-weight:600;color:var(--ink-3);
|
||
padding:7px 20px 7px 0;border-bottom:1px solid var(--line-2);white-space:nowrap;
|
||
}
|
||
.fields dd{font-size:14.5px;padding:7px 0;border-bottom:1px solid var(--line-2)}
|
||
.fields dt:last-of-type,.fields dd:last-of-type{border-bottom:none}
|
||
|
||
.val{transition:.3s}
|
||
.val.empty{
|
||
display:inline-block;font-family:'Inter Tight',sans-serif;font-size:11.5px;
|
||
color:var(--amber);background:var(--amber-bg);border:1px dashed #dcc08a;
|
||
padding:1px 8px;border-radius:2px;font-style:normal;
|
||
}
|
||
.val.fresh{animation:pop .5s ease both}
|
||
@keyframes pop{0%{background:#fff3d4}100%{background:transparent}}
|
||
|
||
.mono{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500}
|
||
|
||
/* tabela de entregáveis */
|
||
table.ent{width:100%;border-collapse:collapse;font-family:'Inter Tight',sans-serif;margin-top:4px}
|
||
table.ent th{
|
||
font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);
|
||
text-align:left;padding:0 8px 7px 0;border-bottom:1px solid var(--ink);
|
||
}
|
||
table.ent th.n,table.ent td.n{text-align:right;padding-right:0}
|
||
table.ent td{font-size:12.8px;padding:9px 8px 9px 0;border-bottom:1px solid var(--line-2);vertical-align:top}
|
||
table.ent td.n{font-family:'JetBrains Mono',monospace;font-weight:500}
|
||
table.ent tr.total td{border-bottom:none;border-top:2px solid var(--ink);font-weight:700;padding-top:9px;font-size:13.2px}
|
||
table.ent .nm{font-weight:600;color:var(--ink);display:block;line-height:1.35}
|
||
table.ent .meta{font-size:11px;color:var(--ink-3);display:block;margin-top:2px}
|
||
.tag{
|
||
display:inline-block;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;
|
||
padding:2px 6px;border-radius:2px;background:#e8f4f7;color:var(--teal);
|
||
}
|
||
|
||
/* verificação */
|
||
.check{margin-top:8px;border:1px solid var(--line);background:#fff}
|
||
.check-h{
|
||
font-family:'Inter Tight',sans-serif;font-size:10.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
|
||
padding:10px 14px;border-bottom:1px solid var(--line-2);color:var(--ink-2);display:flex;align-items:center;gap:8px;
|
||
}
|
||
.check-h .score{margin-left:auto;font-family:'JetBrains Mono',monospace;color:var(--green)}
|
||
.check ul{list-style:none;padding:8px 14px 12px}
|
||
.check li{
|
||
font-family:'Inter Tight',sans-serif;font-size:12.5px;padding:5px 0 5px 22px;position:relative;color:var(--ink-2);line-height:1.45;
|
||
}
|
||
.check li::before{position:absolute;left:0;top:5px;font-family:'JetBrains Mono',monospace;font-size:12px}
|
||
.check li.ok::before{content:'✓';color:var(--green)}
|
||
.check li.warn::before{content:'!';color:var(--amber);font-weight:700;left:4px}
|
||
.check li.warn{color:#6b4a12}
|
||
|
||
/* empty state doc */
|
||
.doc-empty{
|
||
max-width:660px;margin:0 auto;padding:70px 40px;text-align:center;
|
||
border:1px dashed #c3cad4;background:#f7f8fa;
|
||
}
|
||
.doc-empty h3{font-family:'Newsreader',serif;font-size:21px;font-weight:500;color:var(--ink-2);margin-bottom:9px}
|
||
.doc-empty p{font-size:13.5px;color:var(--ink-3);max-width:380px;margin:0 auto;line-height:1.6}
|
||
|
||
/* painel base */
|
||
.kb{padding:22px 26px;overflow-y:auto;flex:1;background:#f1f3f6}
|
||
.kb-sec{margin-bottom:26px}
|
||
.kb-sec>h3{
|
||
font-size:10.5px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);
|
||
margin-bottom:11px;padding-bottom:6px;border-bottom:1px solid var(--line);
|
||
}
|
||
.rec{
|
||
background:#fff;border:1px solid var(--line);padding:12px 14px;margin-bottom:8px;
|
||
display:flex;gap:13px;align-items:flex-start;transition:.18s;
|
||
}
|
||
.rec.hit{border-color:var(--teal);box-shadow:0 0 0 2px #d9eef3}
|
||
.rec .rid{font-family:'JetBrains Mono',monospace;font-size:11.5px;font-weight:700;color:var(--ink);flex:none;width:44px}
|
||
.rec .rbody{flex:1;min-width:0}
|
||
.rec .rt{font-size:13.2px;font-weight:600;line-height:1.35}
|
||
.rec .rm{font-size:11.5px;color:var(--ink-3);margin-top:3px;display:flex;gap:9px;flex-wrap:wrap}
|
||
.rec .rm span{white-space:nowrap}
|
||
.kb-note{font-size:12.5px;color:var(--ink-3);line-height:1.55;padding:10px 12px;background:#fff;border-left:2px solid var(--line)}
|
||
|
||
@media(max-width:1000px){
|
||
body{overflow:auto}
|
||
.app{height:auto}
|
||
.main{grid-template-columns:1fr}
|
||
.left{border-right:none;border-bottom:1px solid var(--line);height:auto}
|
||
.stream{max-height:none}
|
||
.doc{padding:26px 22px 30px}
|
||
.doc-wrap{padding:16px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
|
||
<header class="topbar">
|
||
<div class="logo">
|
||
<svg width="19" height="21" viewBox="0 0 20 22" aria-hidden="true">
|
||
<path d="M10 1l8 4.6v9.2L10 20.4 2 14.8V5.6z" fill="#3fb6d0"/>
|
||
<path d="M10 5.6l4.3 2.5v4.9L10 15.5l-4.3-2.5V8.1z" fill="#0f2338"/>
|
||
</svg>
|
||
Redator de Demandas
|
||
</div>
|
||
<div class="sep"></div>
|
||
<div class="ctx">Contrato <b>Pregão 29/2025</b> · SES-MG · Rede Mineira de Dados em Saúde</div>
|
||
<div class="right">
|
||
<div class="pill" id="pill">Rascunho</div>
|
||
<button class="btn-reset" onclick="location.reload()">Recomeçar</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main">
|
||
|
||
<!-- ========== ESQUERDA ========== -->
|
||
<section class="left">
|
||
<div class="left-head">
|
||
<div class="eyebrow">Entrevista</div>
|
||
<div class="step" id="stepLabel">passo 0 de 6</div>
|
||
</div>
|
||
|
||
<div class="stream" id="stream"></div>
|
||
|
||
<div class="composer" id="composer">
|
||
<textarea id="input" placeholder="Cole aqui a demanda como ela chegou — trecho de ata, e-mail, mensagem de Teams. Não precisa organizar."></textarea>
|
||
<div class="composer-row">
|
||
<button class="btn" id="send">Analisar demanda</button>
|
||
<button class="btn ghost" id="example">Usar um exemplo</button>
|
||
<span class="hint">Nada é enviado — protótipo local</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ========== DIREITA ========== -->
|
||
<section class="right">
|
||
<div class="tabs">
|
||
<button class="tab on" data-tab="doc">Demanda em construção</button>
|
||
<button class="tab" data-tab="kb">Base consultada <span class="n" id="kbCount">0</span></button>
|
||
</div>
|
||
|
||
<div class="doc-wrap" id="docWrap">
|
||
<div class="doc-empty" id="docEmpty">
|
||
<h3>A demanda se escreve enquanto vocês conversam</h3>
|
||
<p>Cole o texto bruto ao lado. Cada resposta preenche um campo aqui — enquadramento, entregáveis, USTs e critérios de aceite.</p>
|
||
</div>
|
||
<div class="doc" id="doc" style="display:none"></div>
|
||
</div>
|
||
|
||
<div class="kb" id="kb" style="display:none"></div>
|
||
</section>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
BASE DE CONHECIMENTO SIMULADA
|
||
============================================================ */
|
||
const BASE_ENTREGAS = [
|
||
{id:'SD25', t:'Índice de Capacidade de Integração Municipal', item:'I-05', ust:320, dur:'6 sem', status:'Entregue', tags:['dashboard','score','regulação','municípios']},
|
||
{id:'SD24', t:'Portal RMDS — prototipagem e nota técnica', item:'I-02', ust:240, dur:'6 sem', status:'Pausado', tags:['portal','adesão','municípios']},
|
||
{id:'SD22', t:'Ingestão SUS-Fácil — conector e camada bronze', item:'I-02', ust:400, dur:'10 sem', status:'Entregue', tags:['ingestão','regulação','conector','fila']},
|
||
{id:'SD19', t:'Automação de giro de leito CORE × FHEMIG', item:'I-04', ust:520, dur:'10 sem', status:'Em execução', tags:['leitos','automação','regulação']},
|
||
{id:'SD17', t:'Busca ativa Outubro Rosa — coorte de mamografia', item:'I-05', ust:480, dur:'9 sem', status:'Entregue', tags:['coorte','fila','algoritmo','painel']},
|
||
{id:'SD12', t:'Motor de regras vacinais — MG App Vacina em Dia', item:'I-05', ust:440, dur:'9 sem', status:'Entregue', tags:['motor','regras','algoritmo']}
|
||
];
|
||
|
||
const BASE_PLATAFORMA = [
|
||
{id:'CAP-01', t:'Camada de ingestão e conectores', d:'Conectores para SUS-Fácil, Vivver, Sisreg e e-SUS já homologados. Fonte nova entra como I-02.'},
|
||
{id:'CAP-04', t:'Curadoria semântica e de-para', d:'Harmonização de nomenclatura entre bases não padronizadas. Disponível como serviço.'},
|
||
{id:'CAP-07', t:'Produtos analíticos e painéis', d:'Publicação de dashboards na plataforma com controle de acesso por perfil. Enquadra em I-05.'},
|
||
{id:'CAP-09', t:'Camada de fila e tempo de espera', d:'Ainda não construída. Depende de ingestão contínua do SUS-Fácil (SD22) e do CORE.'}
|
||
];
|
||
|
||
const REGRAS = {
|
||
timebox:{ 'I-02':{c:40}, 'I-03':{c:40}, 'I-04':{c:80}, 'I-05':{c:80} },
|
||
nomes:{
|
||
'I-02':'Desenvolvimento e implantação de plataforma',
|
||
'I-03':'Capacitação e consultoria pós-implantação',
|
||
'I-04':'Manutenção, integração e automação',
|
||
'I-05':'Serviços técnicos de inteligência e produtos analíticos'
|
||
}
|
||
};
|
||
|
||
const DEMANDA_EXEMPLO =
|
||
`Pessoal, na reunião de ontem a SUBRAS trouxe de novo a questão da fila de cirurgias eletivas. Hoje ninguém sabe dizer quantas pessoas estão esperando, há quanto tempo, e em qual município elas estão paradas. O secretário pediu uma visão do estado inteiro.
|
||
|
||
O Guilherme falou que os dados do SUS-Fácil já estão entrando na plataforma desde a integração do mês passado, então em tese dá pra montar alguma coisa. Precisamos definir o que conta como "fila", porque tem solicitação em busca de leito, tem autorizada sem agendamento, tem paciente que já foi operado e não deu baixa.
|
||
|
||
Também precisa cruzar com o Opera Mais pra saber quais municípios recebem incentivo e têm fila grande. A ideia é ter um painel que o pessoal da regulação abra e consiga priorizar. Prazo: querem alguma coisa até o fim do próximo mês.`;
|
||
|
||
/* ============================================================
|
||
ESTADO
|
||
============================================================ */
|
||
let step = 0;
|
||
const sd = {
|
||
titulo:null, projeto:'RMDS — Produto Analítico de Regulação',
|
||
problema:null, item:null, entregaveis:[], artefatos:null, gerada:false
|
||
};
|
||
const kbHits = new Set();
|
||
|
||
const stream = document.getElementById('stream');
|
||
const composer = document.getElementById('composer');
|
||
const input = document.getElementById('input');
|
||
const doc = document.getElementById('doc');
|
||
const docEmpty = document.getElementById('docEmpty');
|
||
|
||
/* ============================================================
|
||
HELPERS DE UI
|
||
============================================================ */
|
||
function msg(who, html, cls=''){
|
||
const d = document.createElement('div');
|
||
d.className = 'msg ' + cls;
|
||
d.innerHTML = `<div class="who"><i></i>${who}</div><div class="bubble">${html}</div>`;
|
||
stream.appendChild(d);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
return d;
|
||
}
|
||
function think(cb, ms=750){
|
||
const d = document.createElement('div');
|
||
d.className='msg';
|
||
d.innerHTML = `<div class="who"><i></i>Redator</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>`;
|
||
stream.appendChild(d);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
setTimeout(()=>{ d.remove(); cb(); }, ms);
|
||
}
|
||
function sourcesBlock(list){
|
||
return `<div class="sources"><div class="sources-h">Consultei na base</div>` +
|
||
list.map(s=>`<div class="src"><div class="id">${s.id}</div><div class="txt">${s.txt}</div></div>`).join('') +
|
||
`</div>`;
|
||
}
|
||
function options(list){
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'opts';
|
||
list.forEach(o=>{
|
||
const b = document.createElement('button');
|
||
b.className = 'opt' + (o.wide?' wide':'');
|
||
b.innerHTML = `${o.label}${o.sub?`<small>${o.sub}</small>`:''}`;
|
||
b.onclick = ()=>{
|
||
wrap.remove();
|
||
msg('Você', o.echo || o.label, 'user');
|
||
o.go();
|
||
};
|
||
wrap.appendChild(b);
|
||
});
|
||
stream.lastElementChild.querySelector('.bubble').appendChild(wrap);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
}
|
||
function setStep(n){
|
||
step = n;
|
||
document.getElementById('stepLabel').textContent = `passo ${n} de 6`;
|
||
}
|
||
function hit(id){ kbHits.add(id); document.getElementById('kbCount').textContent = kbHits.size; renderKB(); }
|
||
|
||
/* ============================================================
|
||
FLUXO DA ENTREVISTA
|
||
============================================================ */
|
||
document.getElementById('example').onclick = ()=>{ input.value = DEMANDA_EXEMPLO; input.focus(); };
|
||
document.getElementById('send').onclick = ()=>{
|
||
const v = input.value.trim();
|
||
if(!v) return;
|
||
composer.style.display='none';
|
||
msg('Você', v.split('\n\n').map(p=>`<p>${p}</p>`).join(''), 'user');
|
||
setStep(1);
|
||
q1();
|
||
};
|
||
|
||
/* ---- 1. leitura e primeira devolutiva ---- */
|
||
function q1(){
|
||
think(()=>{
|
||
['SD22','SD17','CAP-09'].forEach(hit);
|
||
msg('Redator', `
|
||
<p>Li a demanda. O que eu entendi: a SES-MG não consegue enxergar a fila de cirurgias eletivas do estado — nem volume, nem tempo de espera, nem distribuição por município — e quer um painel que permita priorizar.</p>
|
||
<p>Antes de enquadrar, tem uma coisa que preciso confirmar, porque muda o item e o custo.</p>
|
||
${sourcesBlock([
|
||
{id:'SD22', txt:'<b>Ingestão SUS-Fácil</b> — conector e camada bronze entregues. A fonte já está na plataforma, então não é preciso construir ingestão de novo.'},
|
||
{id:'CAP-09', txt:'<b>Camada de fila e tempo de espera</b> — ainda não construída na plataforma. É a peça que falta.'},
|
||
{id:'SD17', txt:'<b>Busca ativa Outubro Rosa</b> — precedente de coorte + painel, enquadrado em I-05, 480 UST.'}
|
||
])}
|
||
<p style="margin-top:14px">A definição de fila — o que conta como paciente esperando — já existe em algum lugar?</p>
|
||
`, 'bot');
|
||
options([
|
||
{label:'Não existe, precisa ser construída com a SUBRAS', sub:'Vai exigir descoberta negocial e validação de regra', wide:true, echo:'Não existe. Vamos ter que definir com eles.', go:()=>q2('nova')},
|
||
{label:'Existe, mas está em documento antigo e desatualizado', sub:'Descoberta mais curta, só validação', wide:true, echo:'Existe algo antigo, mas precisa revalidar.', go:()=>q2('revisar')}
|
||
]);
|
||
}, 1100);
|
||
}
|
||
|
||
/* ---- 2. natureza do trabalho → item ---- */
|
||
function q2(origem){
|
||
sd.problema = origem;
|
||
setStep(2);
|
||
think(()=>{
|
||
msg('Redator', `
|
||
<p>Certo. Então a demanda tem duas naturezas possíveis e eu preciso saber onde está o peso do trabalho — é isso que define o item do contrato.</p>
|
||
<p class="quiet">O painel em si é inteligência. Mas se for preciso trazer fonte nova ou construir conector, aí entra plataforma, e vira entregável separado.</p>
|
||
`, 'bot');
|
||
options([
|
||
{label:'Só o painel e a regra de fila', sub:'Os dados do SUS-Fácil já estão na plataforma', wide:true, echo:'Só painel e regra. Os dados já estão lá.', go:()=>q3('I-05')},
|
||
{label:'Painel + trazer o CORE Saúde como fonte nova', sub:'Cruza dois itens — I-02 para a ingestão, I-05 para o painel', wide:true, echo:'Vamos precisar do CORE também.', go:()=>q3('duplo')}
|
||
]);
|
||
});
|
||
}
|
||
|
||
/* ---- 3. enquadramento ---- */
|
||
function q3(modo){
|
||
sd.item = modo === 'duplo' ? 'I-05 + I-02' : 'I-05';
|
||
setStep(3);
|
||
think(()=>{
|
||
let extra = '';
|
||
if(modo === 'duplo'){
|
||
extra = `<div class="flag"><b>Atenção.</b> Trazer o CORE agora abre uma frente de I-02 com dependência externa — a SUBASS informou que a extração do CORE está suspensa por ajustes no sistema novo. Recomendo abrir como SD separada e não travar o painel.</div>`;
|
||
hit('SD22');
|
||
}
|
||
hit('CAP-07');
|
||
msg('Redator', `
|
||
<p>Enquadrei em <b>I-05</b>. Painel gerencial, algoritmo de classificação e priorização — é a regra do manual: dashboard, índice ou análise vai em I-05, não em I-04.</p>
|
||
${extra}
|
||
${sourcesBlock([
|
||
{id:'CAP-07', txt:'<b>Produtos analíticos e painéis</b> — publicação com controle de acesso por perfil já é capacidade da plataforma. Não precisa ser construída.'},
|
||
{id:'SD17', txt:'Mesmo enquadramento e escopo comparável: coorte + painel de priorização em I-05.'}
|
||
])}
|
||
<p style="margin-top:14px">Agora os entregáveis. Quantas frentes de trabalho você enxerga até o painel no ar?</p>
|
||
`, 'bot');
|
||
options([
|
||
{label:'Três — descobrir a regra, desenhar o painel, construir', sub:'Padrão da maioria das SDs de produto analítico', wide:true, echo:'Três: descoberta, design e construção.', go:()=>q4('tres')},
|
||
{label:'Quatro — com arquitetura do modelo de dados separada', sub:'Se a modelagem da fila for complexa o bastante', wide:true, echo:'Quatro, incluindo arquitetura.', go:()=>q4('quatro')}
|
||
]);
|
||
});
|
||
}
|
||
|
||
/* ---- 4. dimensionamento ---- */
|
||
function q4(qtd){
|
||
setStep(4);
|
||
const base = [
|
||
{n:'Discovery da regra de fila cirúrgica', tipo:'Descoberta', item:'I-05', sem:3, tb:40},
|
||
{n:'Especificação do painel de priorização', tipo:'Design', item:'I-05', sem:2, tb:40}
|
||
];
|
||
if(qtd === 'quatro') base.push({n:'Modelo de dados da fila e tempo de espera', tipo:'Arquitetura', item:'I-05', sem:2, tb:40});
|
||
base.push({n:'Painel de fila cirúrgica publicado', tipo:'Construção', item:'I-05', sem:3, tb:80});
|
||
sd.entregaveis = base;
|
||
renderDoc();
|
||
|
||
think(()=>{
|
||
const total = base.reduce((a,e)=>a+e.sem*e.tb,0);
|
||
msg('Redator', `
|
||
<p>Montei o quadro ao lado. Dimensionei a descoberta em 3 semanas porque definir fila com a SUBRAS envolve área finalística e casos de borda — solicitação em busca de leito, autorizada sem agendamento, alta sem baixa. Isso não fecha em duas.</p>
|
||
<p>Dá <b class="mono">${total} UST</b>. A construção pesa porque em I-05 o time-box é de 80h por semana.</p>
|
||
<div class="flag"><b>Prazo.</b> A demanda pede o painel até o fim do próximo mês. Esse desenho leva ${base.reduce((a,e)=>a+e.sem,0)} semanas encadeadas. Ou a expectativa é recalibrada, ou o discovery cai para 2 semanas com escopo de fila reduzido a cirurgias eletivas apenas.</div>
|
||
<p style="margin-top:14px">Última coisa: o que já existe hoje que sirva de comprovação?</p>
|
||
`, 'bot');
|
||
options([
|
||
{label:'Ata da reunião com a SUBRAS e o pedido do secretário', sub:'Lastro para a descoberta', wide:true, echo:'Tenho a ata e o registro do pedido.', go:()=>q5(true)},
|
||
{label:'Nada formalizado ainda — a demanda veio em reunião', sub:'A SD abre a demanda antes da execução', wide:true, echo:'Nada ainda. É demanda nova.', go:()=>q5(false)}
|
||
]);
|
||
}, 900);
|
||
}
|
||
|
||
/* ---- 5. fechamento ---- */
|
||
function q5(temLastro){
|
||
sd.artefatos = temLastro;
|
||
setStep(5);
|
||
think(()=>{
|
||
msg('Redator', `
|
||
<p>${temLastro
|
||
? 'Bom. A ata entra como artefato comprobatório do entregável de descoberta e ancora a data de abertura.'
|
||
: 'Então essa SD abre demanda futura, não formaliza execução passada. Isso simplifica: os artefatos são projetados, não recuperados.'}</p>
|
||
<p>Tenho o suficiente para escrever. Vou gerar a SD com quadro de entregáveis, enquadramento no TR, artefatos e critérios de aceite por entregável.</p>
|
||
`, 'bot');
|
||
options([
|
||
{label:'Gerar a demanda', wide:true, echo:'Pode gerar.', go:()=>gerar()}
|
||
]);
|
||
});
|
||
}
|
||
|
||
function gerar(){
|
||
setStep(6);
|
||
document.getElementById('pill').textContent = 'Pronta para revisão';
|
||
document.getElementById('pill').style.background = 'rgba(63,182,208,.2)';
|
||
document.getElementById('pill').style.color = '#7fd8ea';
|
||
sd.gerada = true;
|
||
sd.titulo = 'Painel de Fila Cirúrgica Eletiva';
|
||
renderDoc();
|
||
think(()=>{
|
||
msg('Redator', `
|
||
<p>Pronta. Rodei o checklist de validação e passou em tudo, com uma ressalva registrada ao lado sobre o prazo.</p>
|
||
<p class="quiet">O que você faz agora: revisa o contexto — é a parte que precisa da sua leitura política —, confirma o título literal do item contra o TR e exporta.</p>
|
||
<div class="flag"><b>Nesta versão do protótipo</b> a exportação para .docx e o envio ao fluxo de aprovação ainda não estão ligados.</div>
|
||
`, 'bot');
|
||
const wrap = document.createElement('div');
|
||
wrap.className='opts';
|
||
wrap.innerHTML = `<button class="opt" disabled style="opacity:.45;cursor:not-allowed">Exportar .docx</button><button class="opt" disabled style="opacity:.45;cursor:not-allowed">Enviar para aprovação</button>`;
|
||
stream.lastElementChild.querySelector('.bubble').appendChild(wrap);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
}, 1200);
|
||
}
|
||
|
||
/* ============================================================
|
||
RENDER DO DOCUMENTO
|
||
============================================================ */
|
||
function ph(txt){ return `<span class="val empty">${txt}</span>`; }
|
||
|
||
function renderDoc(){
|
||
docEmpty.style.display='none';
|
||
doc.style.display='block';
|
||
|
||
const total = sd.entregaveis.reduce((a,e)=>a+e.sem*e.tb,0);
|
||
const semanas = sd.entregaveis.reduce((a,e)=>a+e.sem,0);
|
||
|
||
let linhas = sd.entregaveis.map((e,i)=>`
|
||
<tr>
|
||
<td class="n" style="width:22px;color:#8b9aab">${i+1}</td>
|
||
<td><span class="nm">${e.n}</span><span class="meta">${e.tipo} · time-box ${e.tb}h/semana</span></td>
|
||
<td style="width:46px"><span class="tag">${e.item}</span></td>
|
||
<td class="n" style="width:52px">${e.sem} sem</td>
|
||
<td class="n" style="width:56px">${e.sem*e.tb}</td>
|
||
</tr>`).join('');
|
||
|
||
if(sd.entregaveis.length){
|
||
linhas += `<tr class="total"><td></td><td>Total</td><td></td><td class="n">${semanas} sem</td><td class="n">${total}</td></tr>`;
|
||
}
|
||
|
||
doc.innerHTML = `
|
||
<div class="doc-head">
|
||
<div class="doc-kicker">Solicitação de Demanda · rascunho automático</div>
|
||
<h1>${sd.titulo ? sd.titulo : '<span class="ph">Título a definir</span>'}</h1>
|
||
</div>
|
||
|
||
<dl class="fields">
|
||
<dt>Projeto</dt><dd>${sd.projeto}</dd>
|
||
<dt>Contratante</dt><dd>Secretaria de Estado de Saúde de Minas Gerais</dd>
|
||
<dt>Contrato</dt><dd class="mono">Pregão Eletrônico nº 29/2025 — Processo 1321127</dd>
|
||
<dt>Item do TR</dt><dd>${sd.item ? `<b>${sd.item}</b> — ${REGRAS.nomes['I-05']}` : ph('aguardando enquadramento')}</dd>
|
||
<dt>Total de UST</dt><dd>${total ? `<span class="mono">${total}</span>` : ph('aguardando entregáveis')}</dd>
|
||
<dt>Entregáveis</dt><dd>${sd.entregaveis.length || ph('—')}</dd>
|
||
</dl>
|
||
|
||
<h2>Contexto e escopo</h2>
|
||
${sd.gerada ? `
|
||
<p>A SES-MG não dispõe hoje de visão consolidada da fila de cirurgias eletivas. Não é possível responder quantos pacientes aguardam, há quanto tempo e em que municípios a espera se concentra — o que impede priorização e enfraquece a gestão do Opera Mais, programa que financia produção cirúrgica cuja demanda represada permanece invisível.</p>
|
||
<p>Os dados do SUS-Fácil já foram integrados à plataforma. O que falta é a camada de interpretação: definir o que constitui fila, tratar os estados intermediários da solicitação e transformar registros de regulação em tempo de espera mensurável por município e procedimento.</p>
|
||
<p>Esta demanda entrega a regra de fila validada com a área finalística e o painel que a torna operável pela regulação estadual.</p>
|
||
` : `<p style="color:#93a1b2;font-style:italic">O contexto é redigido ao final da entrevista, a partir das respostas.</p>`}
|
||
|
||
<h2>Quadro de entregáveis</h2>
|
||
${sd.entregaveis.length ? `
|
||
<table class="ent">
|
||
<thead><tr><th></th><th>Entregável</th><th>Item</th><th class="n">Prazo</th><th class="n">UST</th></tr></thead>
|
||
<tbody>${linhas}</tbody>
|
||
</table>
|
||
` : `<p style="color:#93a1b2;font-style:italic">Os entregáveis aparecem aqui conforme a conversa avança.</p>`}
|
||
|
||
${sd.gerada ? `
|
||
<h2>Enquadramento no Termo de Referência</h2>
|
||
<dl class="fields">
|
||
<dt>Item</dt><dd><b>I-05</b> — ${REGRAS.nomes['I-05']}</dd>
|
||
<dt>Aderência</dt><dd style="font-size:14px;line-height:1.55">
|
||
Concepção do algoritmo de classificação de fila e tempo de espera a partir dos registros de regulação;
|
||
desenvolvimento do modelo de priorização por município e procedimento;
|
||
construção e publicação do painel gerencial com controle de acesso por perfil.
|
||
</dd>
|
||
</dl>
|
||
|
||
<h2>Verificação automática</h2>
|
||
<div class="check">
|
||
<div class="check-h">Checklist do manual <span class="score">10 de 11</span></div>
|
||
<ul>
|
||
<li class="ok">Cada entregável tem item definido</li>
|
||
<li class="ok">Dashboard e algoritmo enquadrados em I-05</li>
|
||
<li class="ok">Nenhum entregável abaixo de 2 nem acima de 4 semanas</li>
|
||
<li class="ok">Time-box de construção correto para I-05 (80h)</li>
|
||
<li class="ok">UST de cada linha confere com time-box × semanas</li>
|
||
<li class="ok">Soma das USTs bate com o cabeçalho</li>
|
||
<li class="ok">Entregáveis encadeados, sem sobreposição de período</li>
|
||
<li class="ok">Produto com nome único ao longo do documento</li>
|
||
<li class="ok">Todo critério de aceite é verificável por terceiro</li>
|
||
<li class="ok">Nenhum campo confessa irregularidade</li>
|
||
<li class="warn">Prazo solicitado pelo demandante é menor que a duração desenhada — precisa de repactuação antes do envio</li>
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
doc.querySelectorAll('.val').forEach(v=>v.classList.add('fresh'));
|
||
}
|
||
|
||
/* ============================================================
|
||
PAINEL DA BASE
|
||
============================================================ */
|
||
function renderKB(){
|
||
const el = document.getElementById('kb');
|
||
el.innerHTML = `
|
||
<div class="kb-sec">
|
||
<h3>Entregas anteriores · ${BASE_ENTREGAS.length} registros</h3>
|
||
${BASE_ENTREGAS.map(r=>`
|
||
<div class="rec ${kbHits.has(r.id)?'hit':''}">
|
||
<div class="rid">${r.id}</div>
|
||
<div class="rbody">
|
||
<div class="rt">${r.t}</div>
|
||
<div class="rm"><span>${r.item}</span><span>${r.ust} UST</span><span>${r.dur}</span><span>${r.status}</span></div>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div class="kb-sec">
|
||
<h3>Capacidades da plataforma</h3>
|
||
${BASE_PLATAFORMA.map(r=>`
|
||
<div class="rec ${kbHits.has(r.id)?'hit':''}">
|
||
<div class="rid">${r.id}</div>
|
||
<div class="rbody">
|
||
<div class="rt">${r.t}</div>
|
||
<div class="rm" style="display:block;line-height:1.45">${r.d}</div>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div class="kb-sec">
|
||
<h3>Regras aplicadas</h3>
|
||
<div class="kb-note">Time-box de 40h/semana em Descoberta, Design e Arquitetura. Construção a 40h em I-02 e I-03, e a 80h em I-04 e I-05. Cada entregável entre 2 e 4 semanas. Entregáveis de itens diferentes nunca somam na mesma linha.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
renderKB();
|
||
|
||
/* tabs */
|
||
document.querySelectorAll('.tab').forEach(t=>{
|
||
t.onclick = ()=>{
|
||
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));
|
||
t.classList.add('on');
|
||
const isDoc = t.dataset.tab === 'doc';
|
||
document.getElementById('docWrap').style.display = isDoc ? 'block' : 'none';
|
||
document.getElementById('kb').style.display = isDoc ? 'none' : 'block';
|
||
};
|
||
});
|
||
|
||
/* abertura */
|
||
msg('Redator', `
|
||
<p>Me conta a demanda do jeito que ela chegou. Pode ser trecho de ata, e-mail encaminhado, print de conversa — não precisa organizar nada.</p>
|
||
<p class="quiet">Eu leio, comparo com o que já entregamos e com o que a plataforma já faz, e vou te perguntando só o que ainda falta para fechar a SD.</p>
|
||
`, 'bot');
|
||
</script>
|
||
</body>
|
||
</html>
|