intsoft commited on
Commit
7154cbf
·
verified ·
1 Parent(s): 0ff3238

Publicar ferramenta Comenta de avaliação local v0.1

Browse files
Files changed (8) hide show
  1. LICENSE +9 -0
  2. README.md +53 -4
  3. config.js +26 -0
  4. engine.js +90 -0
  5. index.html +1 -19
  6. style.css +1 -28
  7. test-cases.json +40 -0
  8. ui.js +26 -0
LICENSE ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Comenta / intsoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.md CHANGED
@@ -1,10 +1,59 @@
1
  ---
2
- title: Comenta Chat Interno
3
- emoji: 👁
4
- colorFrom: gray
5
  colorTo: purple
6
  sdk: static
 
7
  pinned: false
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "Comenta · Chat interno da equipe"
3
+ emoji: 💬
4
+ colorFrom: pink
5
  colorTo: purple
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
+ license: mit
10
+ short_description: "Ferramenta Comenta gratuita, local e verificável"
11
+ tags:
12
+ - comenta
13
+ - portuguese
14
+ - browser-only
15
+ - rule-based
16
  ---
17
 
18
+ # Comenta — Chat interno da equipe
19
+
20
+ ## Status real
21
+ Aplicativo público de avaliação funcional, versão 0.1. **Não é um modelo treinado.**
22
+ Ferramenta/agente determinístico de código novo. Nenhum adaptador experimental ou código de terceiros foi incluído.
23
+
24
+ **Não conectado aos canais reais.** Nenhuma ação externa, envio, transferência real ou acesso a dados de produção.
25
+
26
+ ## Recurso de origem
27
+ Card público na seção Recursos: https://comenta.com.br/#recursos
28
+
29
+ > Combine a resposta com o colega sem sair da plataforma nem abrir outro aplicativo.
30
+
31
+ O texto acima descreve a proposta do produto original, **não** promete essas integrações nesta versão.
32
+
33
+ ## O que esta versão faz e o que falta
34
+ Simula uma conversa interna apenas na sua aba. Pode acrescentar mensagens manualmente ao histórico; não conecta colegas, não autentica equipe e não serve para conteúdo confidencial.
35
+
36
+ ## Como testar
37
+ 1. Abra o aplicativo; os campos vêm com dados totalmente fictícios.
38
+ 2. Clique em **Executar avaliação** e examine o JSON de saída.
39
+ 3. Altere os campos e execute novamente; entradas inválidas têm erro explícito.
40
+ 4. **Restaurar exemplo** repõe os dados fictícios; **Limpar dados** esvazia a interface.
41
+ 5. Você pode baixar manualmente o resultado JSON, sem enviá-lo a terceiros.
42
+
43
+ Função principal: `internalChat` em `engine.js`. Os testes funcionais de implantação estão em `test-cases.json`.
44
+
45
+ ## Privacidade e segurança
46
+ - Execução JavaScript somente no navegador. Sem servidor de processamento, `fetch`, analytics, CDN, telemetria de entradas ou armazenamento local.
47
+ - Não cole senhas, tokens, dados pessoais ou conversas confidenciais.
48
+ - Não há autenticação, autorização de equipe, trilha de auditoria ou persistência. Não usar como CRM de produção.
49
+ - Conteúdo fornecido é renderizado com `textContent`, nunca HTML executável.
50
+ - Uma política CSP restringe rede e scripts ao próprio Space; sem execução arbitrária de código.
51
+ - O Hugging Face pode registrar metadados de acesso HTTP; os campos não são enviados por este app.
52
+ - Saídas por regras podem falhar semanticamente. Revisão humana obrigatória.
53
+
54
+ ## Infraestrutura e licença
55
+ Space **Static gratuito**: não usa GPU, créditos de inferência, chaves externas, buckets ou modelos pagos. Não requer PRO.
56
+ Código original sob MIT. Sem pesos ML, sem treinamento e sem dependências de runtime externas.
57
+
58
+ ## Integrações futuras
59
+ A conexão a canais reais exige implementação segura, credenciais específicas, consentimento, proteção LGPD e testes adicionais. Nenhuma dessas integrações foi ativada neste Space.
config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ window.COMENTA_CONFIG = {
2
+ "title": "Chat interno da equipe",
3
+ "description": "Combine a resposta com o colega sem sair da plataforma nem abrir outro aplicativo.",
4
+ "limitation": "Simula uma conversa interna apenas na sua aba. Pode acrescentar mensagens manualmente ao histórico; não conecta colegas, não autentica equipe e não serve para conteúdo confidencial.",
5
+ "fields": [
6
+ {
7
+ "id": "history",
8
+ "label": "Histórico fictício da conversa (JSON)",
9
+ "value": "[\n {\n \"autor\": \"Atendente A (exemplo)\",\n \"texto\": \"Vamos revisar a resposta antes de enviar?\"\n }\n]",
10
+ "json": true
11
+ },
12
+ {
13
+ "id": "author",
14
+ "label": "Autor fictício",
15
+ "value": "Atendente B (exemplo)",
16
+ "type": "input"
17
+ },
18
+ {
19
+ "id": "message",
20
+ "label": "Mensagem local de avaliação",
21
+ "value": "Sim, vou verificar a base de respostas de exemplo."
22
+ }
23
+ ],
24
+ "fn": "internalChat",
25
+ "slug": "comenta-chat-interno"
26
+ };
engine.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+ // Código novo Comenta. Nenhuma chamada de rede, persistência ou execução de entrada.
3
+ const norm = s => String(s ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase();
4
+ function text(v, label='Texto') { const s=String(v??'').trim(); if(!s)throw Error(label+': preencha o campo.'); if(s.length>30000)throw Error(label+': limite de 30 mil caracteres.');return s; }
5
+ function parse(s) {try{return JSON.parse(text(s,'JSON'));}catch(e){throw Error('JSON inválido: use aspas duplas e confira o exemplo.');}}
6
+ function list(s) {const a=parse(s);if(!Array.isArray(a)||a.length>300)throw Error('Informe uma lista JSON com até 300 itens.');return a;}
7
+ function str(v,label){if(typeof v!=='string'||!v.trim()||v.length>3000)throw Error('Campo '+label+' deve ser texto de 1 a 3000 caracteres.');return v.trim();}
8
+ function classify(message) {
9
+ const s=norm(text(message));
10
+ const defs=[['Financeiro',['boleto','pagamento','cobranca','fatura','reembolso','pix']],['Suporte',['erro','falha','travou','nao funciona','acesso','senha']],['Vendas',['preco','comprar','orcamento','plano','contratar','proposta']]];
11
+ const scores=defs.map(([name,words])=>({name,hits:words.filter(w=>s.includes(w))})).sort((a,b)=>b.hits.length-a.hits.length);
12
+ const risk=['fraude','vazamento','ameaca','discriminacao','processo','urgente','cancelar'].filter(w=>s.includes(w));
13
+ const match=scores[0];const tie=match.hits.length&&scores[1].hits.length===match.hits.length;
14
+ return {departamento:match.hits.length&&!tie?match.name:'Triagem humana',prioridade:risk.length?'Alta':'Normal',evidencias:match.hits,alertas:risk,metodo:'Regras por palavras-chave; não usa Claude nem modelo treinado',revisao_humana:!match.hits.length||!!tie||!!risk.length};
15
+ }
16
+ function summary(transcript, count=4) {
17
+ const s=text(transcript);const lines=s.split(/\n+|(?<=[.!?])\s+/).map(x=>x.trim()).filter(Boolean);
18
+ const n=Number(count);if(!Number.isInteger(n)||n<1||n>8)throw Error('Escolha de 1 a 8 trechos.');
19
+ const freq=new Map();for(const w of norm(s).match(/[a-z]{4,}/g)||[])freq.set(w,(freq.get(w)||0)+1);
20
+ const ranked=lines.map((line,i)=>({line,i,score:(norm(line).match(/[a-z]{4,}/g)||[]).reduce((a,w)=>a+(freq.get(w)||0),0)/Math.sqrt(line.length+1)})).sort((a,b)=>b.score-a.score).slice(0,n).sort((a,b)=>a.i-b.i);
21
+ return {trechos_literais:ranked.map(x=>x.line),pendencias_literais:lines.filter(x=>/aguard|pendente|precis|retorn|verific|falta/.test(norm(x))).slice(0,5),total_trechos:lines.length,metodo:'Resumo extrativo: seleciona trechos do original; não inventa fatos. Não detecta resolução com garantia.'};
22
+ }
23
+ function faqLookup(question, faqJSON) {
24
+ const q=norm(text(question));const faq=list(faqJSON); const words=new Set((q.match(/[a-z0-9]{3,}/g)||[]).filter(w=>!['qual','como','que','para','uma','por','favor','meu','minha','com','voce','quero','saber'].includes(w)));
25
+ let best=null,bestScore=0;
26
+ for(const item of faq){str(item.pergunta,'pergunta');str(item.resposta,'resposta');const keys=[...new Set(norm(item.pergunta).match(/[a-z0-9]{3,}/g)||[])];const matches=keys.filter(k=>words.has(k));const score=matches.length/Math.max(1,keys.length);if(score>bestScore){bestScore=score;best={resposta:item.resposta,fonte:item.pergunta,coincidencias:matches};}}
27
+ return bestScore>=0.4?{...best,correspondencia:Math.round(bestScore*100)+'% dos termos da pergunta cadastrada'}:null;
28
+ }
29
+ function reply(question, faq) {
30
+ const t=classify(question);const found=faqLookup(question,faq);
31
+ return {rascunho:found?'Olá! '+found.resposta+' Posso ajudar em mais alguma dúvida?':'Olá! Recebi sua solicitação. Vou encaminhar para avaliação da equipe. Pode detalhar o que aconteceu, sem incluir senhas ou dados sensíveis?',base:found?.fonte||'Sem resposta na base fornecida',departamento:t.departamento,status:'Rascunho para revisão. Nenhuma mensagem foi enviada.',metodo:'Busca lexical em FAQ fornecida pelo visitante + modelos fixos de resposta'};
32
+ }
33
+ function handoff(question, faq) {
34
+ const t=classify(question);const askHuman=/humano|pessoa|atendente|supervisor/.test(norm(question));const f=faqLookup(question,faq);
35
+ const transfer=askHuman||t.prioridade==='Alta'||!f;
36
+ return {decisao:transfer?'Encaminhar para pessoa':'Resposta de autoatendimento (prévia)',motivo:askHuman?'Pedido explícito de atendimento humano':t.prioridade==='Alta'?'Sinal de risco ou urgência':!f?'Base fornecida não contém correspondência suficiente':'Pergunta localizada na base fornecida',resposta:transfer?'Uma pessoa da equipe precisa revisar este caso.':f.resposta,pacote_handoff:transfer?{mensagem_original:question,triagem:t}:null,acao_externa:'Nenhuma: não cria ticket, não transfere e não envia mensagem.'};
37
+ }
38
+ function inbox(input, filter='Todos') {
39
+ const items=list(input);const allowed=['WhatsApp','Instagram','Messenger','Chat do site','Telegram','E-mail'];
40
+ const groups=new Map();items.forEach((m,i)=>{const contato=str(m.contato,'contato'),canal=str(m.canal,'canal'),mensagem=str(m.mensagem,'mensagem');if(!allowed.includes(canal))throw Error('Canal inválido no item '+(i+1)+'. Use os nomes do exemplo.');if(filter!=='Todos'&&filter!==canal)return;const key=contato;const g=groups.get(key)||{contato,canais:[],mensagens:[]};if(!g.canais.includes(canal))g.canais.push(canal);g.mensagens.push({canal,mensagem,ordem:i+1});groups.set(key,g);});
41
+ return {conversas:[...groups.values()],total_mensagens:[...groups.values()].reduce((a,g)=>a+g.mensagens.length,0),criterio:'Agrupamento por identificador contato fornecido; não concilia identidades entre plataformas.'};
42
+ }
43
+ function moveCards(input,id,status) {
44
+ const a=list(input);const states=['Aguardando','Em atendimento','Resolvida'];if(!states.includes(status))throw Error('Status inválido.');let found=false;
45
+ const ids=new Set();for(const c of a){str(c.id,'id');str(c.titulo,'titulo');if(ids.has(c.id))throw Error('IDs duplicados.');ids.add(c.id);if(!states.includes(c.status))throw Error('Status inválido no quadro.');if(c.id===id){c.status=status;found=true;}}
46
+ if(id&&!found)throw Error('ID não encontrado.');return {cards:a,colunas:Object.fromEntries(states.map(s=>[s,a.filter(c=>c.status===s)])),movimentacao:id?'Status atualizado somente nesta aba.':'Quadro carregado nesta aba.'};
47
+ }
48
+ function queues(message,input) {
49
+ const conf=list(input);const t=classify(message);const q=conf.find(q=>q.departamento===t.departamento);
50
+ for(const x of conf){str(x.departamento,'departamento');str(x.equipe,'equipe');str(x.horario,'horario');}
51
+ return {triagem:t,fila:q||{departamento:'Triagem humana',equipe:'Responsável não configurado',horario:'Verificar manualmente'},acao:'Roteamento sugerido; não atribui conversas reais. Horário é texto informativo; use o Space Horário para cálculo.'};
52
+ }
53
+ function hours(dateISO,zone,start,end,days,message) {
54
+ const date=new Date(text(dateISO,'Data com fuso'));if(!/([zZ]|[+-]\d\d:\d\d)$/.test(dateISO)||!Number.isFinite(+date))throw Error('Informe data ISO válida com fuso, por exemplo 2026-11-09T10:00:00-04:00.');
55
+ const tm=x=>{if(!/^([01]\d|2[0-3]):[0-5]\d$/.test(x))throw Error('Use HH:MM, entre 00:00 e 23:59.');return Number(x.slice(0,2))*60+Number(x.slice(3));};const a=tm(start),b=tm(end);if(a===b)throw Error('Início e fim não podem ser iguais.');
56
+ if(!/^[0-6](\s*,\s*[0-6])*$/.test(String(days).trim()))throw Error('Dias: informe ao menos um número de 0 a 6.');const ds=String(days).split(',').map(x=>Number(x.trim()));if(ds.some(x=>!Number.isInteger(x)||x<0||x>6))throw Error('Dias: use números de 0 (domingo) a 6 (sábado), separados por vírgulas.');
57
+ let p;try{p=Object.fromEntries(new Intl.DateTimeFormat('en-US',{timeZone:zone,weekday:'short',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(date).map(x=>[x.type,x.value]));}catch{throw Error('Fuso IANA inválido.');}
58
+ const day=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].indexOf(p.weekday),mins=+p.hour*60+(+p.minute);
59
+ const open=a<b?(ds.includes(day)&&mins>=a&&mins<b):((ds.includes(day)&&mins>=a)||(ds.includes((day+6)%7)&&mins<b));
60
+ return {aberto:open,horario_local:new Intl.DateTimeFormat('pt-BR',{timeZone:zone,dateStyle:'short',timeStyle:'short'}).format(date),fuso:zone,previa:open?'Dentro do expediente. Encaminhar para a fila.':text(message,'Aviso'),limites:'Considera dias da semana e turnos inclusive noturnos; não considera feriados ou férias. Não envia aviso.'};
61
+ }
62
+ function shortcuts(message,input) {
63
+ const a=parse(input);if(!a||Array.isArray(a)||typeof a!=='object'||Object.keys(a).length>100)throw Error('Atalhos devem ser um objeto JSON com até 100 chaves.');
64
+ const pairs=Object.entries(a);for(const [k,v]of pairs){if(!/^\/[a-z0-9_-]{1,30}$/i.test(k))throw Error('Atalho inválido: use /nome.');str(v,'resposta');}
65
+ const used=[];const out=text(message).replace(/\/[a-z0-9_-]+/gi,key=>{if(Object.hasOwn(a,key)){used.push(key);return a[key];}return key;});
66
+ return {texto_expandido:out,atalhos_usados:used,acao:'Texto local para revisão; nenhum envio.'};
67
+ }
68
+ function notes(message,tags,note) {
69
+ return {visao_interna:{mensagem:text(message),tags:[...new Set(String(tags).split(',').map(x=>x.trim()).filter(Boolean))].slice(0,20),nota_interna:text(note,'Nota')},visao_cliente:{mensagem:text(message)},aviso:'Projeções locais, não controle de acesso. Nesta demonstração o visitante vê ambas. Não registre notas confidenciais reais.'};
70
+ }
71
+ function team(message,faq) {
72
+ const t=classify(message),r=handoff(message,faq);return {etapas:[{agente:'Triagem por regras',saida:t},{agente:'Consulta da base lexical',saida:faqLookup(message,faq)||'Sem correspondência'},{agente:'Supervisor de segurança',saida:r}],agendamento:/agend|reuniao|horario/.test(norm(message))?'Pedido detectado; agenda não conectada. Nenhuma reserva realizada.':'Não solicitado',execucao:'Orquestrador determinístico. Não usa múltiplos LLMs. Nenhuma ação externa.'};
73
+ }
74
+ function nps(input) {
75
+ const a=text(input,'Notas').split(/[,;\s]+/).filter(Boolean).map(Number);if(a.length>10000||a.some(n=>!Number.isInteger(n)||n<0||n>10))throw Error('Notas devem ser inteiros de 0 a 10. Limite: 10 mil notas.');
76
+ const promoters=a.filter(x=>x>=9).length,detractors=a.filter(x=>x<=6).length;return {respostas:a.length,promotores:promoters,neutros:a.length-promoters-detractors,detratores:detractors,nps:Math.round((promoters-detractors)/a.length*10000)/100,formula:'(% notas 9–10) − (% notas 0–6)',pesquisa_sugerida:'De 0 a 10, quanto você recomendaria nosso atendimento?',acao:'Não dispara pesquisas nem importa avaliações reais.'};
77
+ }
78
+ function internalChat(input,author,message) {
79
+ const a=list(input);if(a.length>=100)throw Error('Limite de 100 mensagens por conversa de avaliação.');for(const m of a){str(m.autor,'autor');str(m.texto,'texto');}
80
+ a.push({autor:str(author,'autor'),texto:str(message,'mensagem')});return {conversa_local:a,visao_cliente:[],aviso:'Simulação em uma aba; sem login, criptografia ponta a ponta ou conexão entre colegas. Não use informações internas reais.'};
81
+ }
82
+ async function webhook(payload,key,signature,attempts) {
83
+ parse(payload);const k=text(key,'Chave de teste');if(k.length<8)throw Error('Use chave fictícia com pelo menos 8 caracteres. Não use chave de produção.');const n=Number(attempts);if(!Number.isInteger(n)||n<1||n>8)throw Error('Tentativas: inteiro de 1 a 8.');
84
+ const enc=new TextEncoder();const cryptokey=await globalThis.crypto.subtle.importKey('raw',enc.encode(k),{name:'HMAC',hash:'SHA-256'},false,['sign','verify']);
85
+ const digest=new Uint8Array(await globalThis.crypto.subtle.sign('HMAC',cryptokey,enc.encode(payload)));const hex=Array.from(digest,b=>b.toString(16).padStart(2,'0')).join('');
86
+ let verified=null;if(String(signature).trim()){const s=String(signature).trim().replace(/^sha256=/,'');if(!/^[0-9a-f]{64}$/i.test(s))throw Error('Assinatura esperada: 64 caracteres hexadecimais.');verified=await globalThis.crypto.subtle.verify('HMAC',cryptokey,Uint8Array.from(s.match(/../g),x=>parseInt(x,16)),enc.encode(payload));}
87
+ return {algoritmo:'HMAC-SHA256',assinatura:'sha256='+hex,assinatura_informada_valida:verified,bytes_assinados:enc.encode(payload).length,plano_retry_segundos:Array.from({length:n},(_,i)=>Math.min(3600,2**i*5)),requisicao_exemplo:{metodo:'POST',caminho:'/webhooks/comenta',cabecalhos:{'Content-Type':'application/json','X-Comenta-Signature':'sha256='+hex}},aviso:'Calcula assinatura dos bytes exatos do campo. Não fornece API hospedada, não faz requisições, não entrega eventos e não executa retries. Chave fica apenas em memória nesta aba.'};
88
+ }
89
+ const functions={classify,summary,reply,handoff,inbox,moveCards,queues,hours,shortcuts,notes,team,nps,internalChat,webhook};
90
+ if(typeof module!=='undefined')module.exports=functions;
index.html CHANGED
@@ -1,19 +1 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
1
+ <!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><meta name="referrer" content="no-referrer"><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"><title>Chat interno da equipe — Comenta</title><link rel="stylesheet" href="style.css"></head><body><header><a class="brand" href="https://comenta.com.br/" target="_blank" rel="noopener noreferrer">comenta<i>•</i></a><nav aria-label="Navegação"><a href="https://huggingface.co/intsoft" target="_blank" rel="noopener noreferrer">Projetos intsoft</a><span class="badge">Avaliação local · gratuito</span></nav></header><main><div class="eyebrow">Laboratório Comenta / 13 de 14</div><h1 id="title">Chat interno da equipe</h1><p class="intro" id="description"></p><div class="notice"><strong>Não conectado aos canais reais.</strong> Ferramenta/agente por regras, não modelo de IA treinado. Processa entradas apenas no seu navegador; não envia mensagens. Use exemplos fictícios, nunca credenciais ou dados pessoais.</div><div class="workspace"><section class="panel"><h2>Experimente o recurso</h2><form id="form"><div id="fields"></div><div class="actions"><button id="run" type="submit">Executar avaliação</button><button id="reset" class="secondary" type="button">Restaurar exemplo</button><button id="clear" class="secondary" type="button">Limpar dados</button></div></form><p id="error" class="error" role="alert"></p><div id="extra"></div></section><section class="panel"><h2>Resultado verificável</h2><p class="status" id="resultstatus" aria-live="polite">Aguardando execução</p><pre id="output" aria-label="Resultado">Preencha ou use o exemplo e clique em executar.</pre><p class="small">Nada é enviado automaticamente. Você pode baixar o resultado nesta máquina.</p><button class="secondary" id="download" type="button">Baixar resultado JSON</button><div class="limits"><h2>O que esta versão faz</h2><p id="limitation" class="small"></p><p class="small">Sem armazenamento no servidor, cookies próprios ou histórico compartilhado. Recarregar encerra a sessão. O provedor de hospedagem pode registrar acessos HTTP, mas os campos não são transmitidos por este aplicativo.</p></div></section></div></main><footer>Código original Comenta · Versão de avaliação 0.1 · Regras transparentes no repositório público · Integrações e controle de acesso de produção ainda pendentes.</footer><script src="config.js"></script><script src="engine.js"></script><script src="ui.js"></script></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
style.css CHANGED
@@ -1,28 +1 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }
 
1
+ :root{--ink:#172033;--muted:#526078;--accent:#a21caf;--line:#d9deea;--canvas:#f5f7fb;--panel:#fff;--radius:16px}*{box-sizing:border-box}body{margin:0;background:var(--canvas);color:var(--ink);font:16px/1.6 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{color:#86198f}header{background:var(--panel);border-bottom:1px solid var(--line);padding:18px max(24px,calc((100vw - 1160px)/2));display:flex;justify-content:space-between;align-items:center;gap:16px}.brand{font-weight:850;font-size:26px;letter-spacing:-1px;color:var(--ink);text-decoration:none}.brand i{font-style:normal;color:var(--accent)}.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:12px;font-weight:750;color:var(--muted)}main{max-width:1208px;margin:auto;padding:32px 24px}h1{font-size:clamp(26px,4vw,38px);line-height:1.2;letter-spacing:-.035em;text-wrap:balance;margin:10px 0 16px;max-width:820px}h2{font-size:19px;line-height:1.3;margin:0 0 18px}p{max-width:85ch}.intro{margin:0 0 24px;color:var(--muted)}.notice{padding:14px 18px;border-left:4px solid var(--accent);background:#f9ecfb;font-size:14px;margin:20px 0 28px}.workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:24px;min-width:0}label{display:block;font-weight:650;font-size:14px;margin:0 0 8px}input,textarea,select{font:inherit;background:#fff;color:var(--ink);border:1px solid #b9c1d0;border-radius:8px;display:block;width:100%;padding:10px 12px;margin:0 0 20px}textarea{min-height:110px;resize:vertical;line-height:1.5}textarea[data-json]{font:13px/1.6 ui-monospace,monospace;min-height:180px}button{border:1px solid var(--accent);background:var(--accent);color:white;font:650 14px/1.4 system-ui;padding:12px 18px;border-radius:9px;cursor:pointer;min-height:44px}button.secondary{background:white;color:var(--ink);border-color:#b9c1d0}button:disabled{opacity:.6;cursor:wait}button:hover{filter:brightness(.94)}:focus-visible{outline:3px solid #a21caf;outline-offset:3px}.actions{display:flex;gap:10px;flex-wrap:wrap}.small{font-size:13px;color:var(--muted)}pre{margin:0;font:13px/1.65 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f7fb;padding:18px;border-radius:10px;max-height:620px;overflow:auto}.error{color:#b91c1c;font-weight:600;min-height:24px}.badge{font-size:12px;background:#f2e6f5;color:#701a75;border:1px solid #e1bde8;border-radius:20px;padding:5px 10px;white-space:nowrap}.limits{margin-top:26px;padding-top:20px;border-top:1px solid var(--line)}footer{max-width:1208px;margin:auto;padding:20px 24px 40px;font-size:13px;color:var(--muted)}.board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:18px}.column{min-height:150px;border:1px dashed #b9c1d0;border-radius:10px;padding:12px;background:#f5f7fb}.column h3{font-size:13px}.ticket{background:white;border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:10px;font-size:13px}.ticket select{font-size:12px;padding:6px;margin:8px 0 0}.columns-full{grid-column:1/-1}.status{font-size:13px;color:var(--muted);margin-bottom:14px}nav{display:flex;gap:20px;font-size:14px}@media(max-width:760px){.workspace{grid-template-columns:1fr}.board{grid-template-columns:1fr}.panel{padding:18px}main{padding:24px 16px}header{padding:16px}.badge{display:none}nav{gap:12px}pre{max-height:480px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test-cases.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "function": "internalChat",
3
+ "cases": [
4
+ {
5
+ "name": "Exemplo padrão da interface",
6
+ "args": [
7
+ "[\n {\n \"autor\": \"Atendente A (exemplo)\",\n \"texto\": \"Vamos revisar a resposta antes de enviar?\"\n }\n]",
8
+ "Atendente B (exemplo)",
9
+ "Sim, vou verificar a base de respostas de exemplo."
10
+ ],
11
+ "object": true
12
+ },
13
+ {
14
+ "args": [
15
+ "[]",
16
+ "Exemplo A",
17
+ "Oi"
18
+ ],
19
+ "path": "conversa_local.0.texto",
20
+ "equals": "Oi"
21
+ },
22
+ {
23
+ "args": [
24
+ "[]",
25
+ "Exemplo A",
26
+ "Oi"
27
+ ],
28
+ "path": "visao_cliente.length",
29
+ "equals": 0
30
+ },
31
+ {
32
+ "args": [
33
+ "{}",
34
+ "A",
35
+ "Oi"
36
+ ],
37
+ "error": true
38
+ }
39
+ ]
40
+ }
ui.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+ const $=id=>document.getElementById(id);
3
+ const cfg=window.COMENTA_CONFIG;
4
+ $('title').textContent=cfg.title;$('description').textContent=cfg.description;$('limitation').textContent=cfg.limitation;
5
+ let busy=false,lastResult=null;
6
+ function field(f){const box=document.createElement('div'),lab=document.createElement('label');lab.htmlFor=f.id;lab.textContent=f.label;box.append(lab);let el;
7
+ if(f.options){el=document.createElement('select');for(const value of f.options){const o=document.createElement('option');o.value=value;o.textContent=value;el.append(o);}}
8
+ else {el=document.createElement(f.type==='input'?'input':'textarea');if(f.json)el.dataset.json='true';if(f.password)el.type='password';if(!['signature','card_id'].includes(f.id))el.required=true;}
9
+ el.id=f.id;el.name=f.id;el.value=f.value??'';el.maxLength=30000;el.autocomplete='off';el.spellcheck=false;box.append(el);return box;
10
+ }
11
+ for(const f of cfg.fields)$('fields').append(field(f));
12
+ function values(){return cfg.fields.map(f=>$(f.id).value);}
13
+ function show(result){lastResult=result;$('output').textContent=JSON.stringify(result,null,2);$('resultstatus').textContent='Concluído nesta aba · nenhuma ação externa';if(cfg.fn==='moveCards')renderBoard(result.cards);}
14
+ async function calculate(){if(busy)return;busy=true;$('run').disabled=true;$('error').textContent='';try{show(await functions[cfg.fn](...values()));}catch(e){$('error').textContent=e.message;$('resultstatus').textContent='Confira os campos';lastResult=null;$('output').textContent='Nenhum resultado válido para esta entrada.';}finally{busy=false;$('run').disabled=false;}}
15
+ $('form').addEventListener('submit',e=>{e.preventDefault();calculate();});
16
+ $('reset').addEventListener('click',()=>{for(const f of cfg.fields)$(f.id).value=f.value??'';$('error').textContent='';$('output').textContent='Exemplo restaurado. Clique em executar para avaliar.';$('resultstatus').textContent='Aguardando execução';lastResult=null;if($('board'))$('board').replaceChildren();});
17
+ $('clear').addEventListener('click',()=>{for(const f of cfg.fields){if(!f.options)$(f.id).value='';}$('output').textContent='Dados removidos desta interface.';$('error').textContent='';lastResult=null;if($('board'))$('board').replaceChildren();});
18
+ $('download').addEventListener('click',()=>{if(!lastResult){$('error').textContent='Execute um teste válido antes de exportar.';return;}const blob=new Blob([JSON.stringify(lastResult,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=cfg.slug+'-resultado.json';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);});
19
+ function renderBoard(cards){const root=$('board');if(!root)return;root.replaceChildren();const states=['Aguardando','Em atendimento','Resolvida'];
20
+ function update(id,status){$('cards').value=JSON.stringify(cards,null,2);$('card_id').value=id;$('status').value=status;calculate();}
21
+ for(const status of states){const col=document.createElement('section');col.className='column';const heading=document.createElement('h3');heading.textContent=status;col.append(heading);col.addEventListener('dragover',e=>e.preventDefault());col.addEventListener('drop',e=>{e.preventDefault();update(e.dataTransfer.getData('text/plain'),status);});
22
+ for(const c of cards.filter(c=>c.status===status)){const ticket=document.createElement('article');ticket.className='ticket';ticket.draggable=true;ticket.addEventListener('dragstart',e=>e.dataTransfer.setData('text/plain',c.id));const title=document.createElement('strong');title.textContent=c.titulo;ticket.append(title);const select=document.createElement('select');select.setAttribute('aria-label','Mover '+c.titulo);for(const s of states){const o=document.createElement('option');o.value=s;o.textContent=s;select.append(o);}select.value=status;select.addEventListener('change',()=>update(c.id,select.value));ticket.append(select);col.append(ticket);}root.append(col);}
23
+ }
24
+ if(cfg.fn==='internalChat'){
25
+ const b=document.createElement('button');b.type='button';b.className='secondary';b.textContent='Usar conversa resultante como histórico';b.addEventListener('click',()=>{if(lastResult?.conversa_local)$('history').value=JSON.stringify(lastResult.conversa_local,null,2);else $('error').textContent='Execute primeiro uma mensagem.';});$('extra').append(b);
26
+ }