assistentjuri / index.html
Danielfonseca1212's picture
Update index.html
e06da82 verified
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LEX — Assistente Jurídico · MCP + HF</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Playfair+Display:wght@700;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#07080d;
--bg2:#0d0f18;
--panel:#111422;
--panel2:#161928;
--border:#1e2235;
--border2:#252840;
--gold:#c9a84c;
--gold-dim:rgba(201,168,76,0.1);
--gold-glow:rgba(201,168,76,0.2);
--gold-soft:rgba(201,168,76,0.6);
--cyan:#4ecdc4;
--cyan-dim:rgba(78,205,196,0.08);
--red:#e05c5c;
--red-dim:rgba(224,92,92,0.08);
--green:#5ce08a;
--green-dim:rgba(92,224,138,0.08);
--purple:#9b8fff;
--text:#cdd0e0;
--text2:#7a7f9a;
--text3:#3e4260;
--mono:'IBM Plex Mono',monospace;
--serif:'Playfair Display',serif;
--r:8px;
}
html{height:100%}
body{
background:var(--bg);
color:var(--text);
font-family:var(--mono);
font-size:13px;
height:100%;
overflow:hidden;
display:flex;
flex-direction:column;
}
/* ── SCAN LINES ── */
body::after{
content:'';
position:fixed;
inset:0;
background:repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.08) 2px,
rgba(0,0,0,0.08) 4px
);
pointer-events:none;
z-index:9999;
}
/* ── TOP BAR ── */
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
padding:0 20px;
height:48px;
background:var(--bg2);
border-bottom:1px solid var(--border);
flex-shrink:0;
position:relative;
z-index:10;
}
.topbar-left{display:flex;align-items:center;gap:14px}
.logo-mark{
font-family:var(--serif);
font-size:20px;
font-weight:900;
color:var(--gold);
letter-spacing:0.05em;
text-shadow:0 0 20px var(--gold-glow);
line-height:1;
}
.logo-sub{
font-size:10px;
color:var(--text3);
letter-spacing:0.15em;
text-transform:uppercase;
border-left:1px solid var(--border2);
padding-left:14px;
}
.topbar-status{display:flex;align-items:center;gap:20px}
.status-pill{
display:flex;align-items:center;gap:6px;
font-size:10px;color:var(--text2);
letter-spacing:0.08em;
}
.status-dot{width:6px;height:6px;border-radius:50%}
.dot-live{background:var(--green);box-shadow:0 0 6px var(--green)}
.dot-hf{background:var(--gold);box-shadow:0 0 6px var(--gold-glow)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.dot-live,.dot-hf{animation:pulse 2.4s ease-in-out infinite}
.topbar-right{display:flex;align-items:center;gap:10px}
.kbd{
font-size:10px;color:var(--text3);
border:1px solid var(--border2);
border-radius:4px;padding:2px 6px;
letter-spacing:0.05em;
}
/* ── LAYOUT ── */
.workspace{
display:flex;
flex:1;
overflow:hidden;
}
/* ── LEFT SIDEBAR ── */
.sidebar{
width:220px;
flex-shrink:0;
background:var(--bg2);
border-right:1px solid var(--border);
display:flex;
flex-direction:column;
overflow:hidden;
}
.sidebar-section{
padding:14px 16px 10px;
border-bottom:1px solid var(--border);
}
.sidebar-label{
font-size:9px;letter-spacing:0.2em;text-transform:uppercase;
color:var(--text3);margin-bottom:10px;
}
.tool-item{
display:flex;align-items:flex-start;gap:8px;
padding:8px 10px;border-radius:6px;
cursor:pointer;transition:background 0.15s;
margin-bottom:2px;
}
.tool-item:hover,.tool-item.active{background:var(--panel)}
.tool-icon{
font-size:14px;margin-top:1px;flex-shrink:0;
width:18px;text-align:center;
}
.tool-info .tool-name{
font-size:11px;font-weight:600;color:var(--text);
line-height:1.3;
}
.tool-info .tool-desc{
font-size:9px;color:var(--text3);
line-height:1.4;margin-top:1px;
}
.tool-item[data-tool="search_legal_models"] .tool-icon{color:var(--gold)}
.tool-item[data-tool="explore_legal_dataset"] .tool-icon{color:var(--cyan)}
.tool-item[data-tool="analyze_legal_text"] .tool-icon{color:var(--purple)}
.tool-item[data-tool="find_jurisprudence"] .tool-icon{color:var(--green)}
.sidebar-footer{
padding:14px 16px;margin-top:auto;
border-top:1px solid var(--border);
font-size:9px;color:var(--text3);
line-height:1.7;
}
.hf-badge{
display:inline-flex;align-items:center;gap:4px;
background:var(--gold-dim);color:var(--gold-soft);
border:1px solid rgba(201,168,76,0.2);
border-radius:4px;padding:2px 6px;
font-size:9px;margin-bottom:8px;
}
/* ── CENTER CHAT ── */
.chat-area{
flex:1;
display:flex;flex-direction:column;
overflow:hidden;
min-width:0;
}
.chat-messages{
flex:1;
overflow-y:auto;
padding:24px 28px;
display:flex;flex-direction:column;gap:20px;
}
.chat-messages::-webkit-scrollbar{width:4px}
.chat-messages::-webkit-scrollbar-track{background:transparent}
.chat-messages::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
/* Welcome */
.welcome{
display:flex;flex-direction:column;align-items:center;
justify-content:center;flex:1;
text-align:center;padding:40px;
gap:16px;
animation:fadeIn 0.6s ease;
}
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.welcome-logo{
font-family:var(--serif);font-size:72px;font-weight:900;
color:var(--gold);line-height:1;
text-shadow:0 0 40px var(--gold-glow),0 0 80px rgba(201,168,76,0.1);
}
.welcome-title{
font-family:var(--serif);font-size:22px;color:var(--text);
font-weight:700;
}
.welcome-sub{font-size:12px;color:var(--text2);max-width:440px;line-height:1.7}
.suggestion-grid{
display:grid;grid-template-columns:1fr 1fr;gap:8px;
width:100%;max-width:540px;margin-top:8px;
}
.suggestion{
background:var(--panel);border:1px solid var(--border);
border-radius:var(--r);padding:12px 14px;
cursor:pointer;transition:all 0.15s;text-align:left;
}
.suggestion:hover{background:var(--panel2);border-color:var(--border2)}
.suggestion .s-icon{font-size:16px;margin-bottom:6px;display:block}
.suggestion .s-text{font-size:11px;color:var(--text);line-height:1.5}
/* Messages */
.msg{display:flex;gap:12px;animation:fadeIn 0.3s ease}
.msg.user{flex-direction:row-reverse}
.msg-avatar{
width:28px;height:28px;border-radius:6px;
display:flex;align-items:center;justify-content:center;
font-size:12px;flex-shrink:0;margin-top:2px;
font-weight:700;
}
.msg.user .msg-avatar{background:var(--gold-dim);color:var(--gold);border:1px solid rgba(201,168,76,0.25)}
.msg.assistant .msg-avatar{background:var(--panel2);color:var(--text2);border:1px solid var(--border)}
.msg-body{max-width:600px;display:flex;flex-direction:column;gap:6px}
.msg.user .msg-body{align-items:flex-end}
.msg-bubble{
background:var(--panel);border:1px solid var(--border);
border-radius:var(--r);padding:12px 16px;
font-size:13px;line-height:1.7;color:var(--text);
}
.msg.user .msg-bubble{
background:var(--gold-dim);border-color:rgba(201,168,76,0.2);
color:var(--text);
}
.msg-bubble p{margin-bottom:8px}
.msg-bubble p:last-child{margin-bottom:0}
.msg-bubble strong{color:var(--gold);font-weight:600}
.msg-bubble code{
background:var(--bg2);border:1px solid var(--border);
border-radius:4px;padding:1px 5px;
font-size:11px;color:var(--cyan);
}
.msg-bubble ul{padding-left:16px;margin:6px 0}
.msg-bubble li{margin-bottom:4px}
/* Tool call card */
.tool-call-card{
background:var(--bg2);border:1px solid var(--border);
border-radius:var(--r);padding:12px 14px;
font-size:11px;display:flex;flex-direction:column;gap:6px;
}
.tc-header{display:flex;align-items:center;gap:8px;color:var(--text2)}
.tc-name{font-weight:600;font-size:12px}
.tc-name.gold{color:var(--gold)}
.tc-name.cyan{color:var(--cyan)}
.tc-name.purple{color:var(--purple)}
.tc-name.green{color:var(--green)}
.tc-arrow{color:var(--text3);font-size:10px}
.tc-status{margin-left:auto;font-size:9px;letter-spacing:0.1em}
.tc-status.running{color:var(--gold);animation:pulse 1.2s infinite}
.tc-status.done{color:var(--green)}
.tc-status.error{color:var(--red)}
.tc-params{
background:var(--panel);border-radius:4px;
padding:6px 8px;color:var(--text3);
font-size:10px;line-height:1.6;
border-left:2px solid var(--border2);
}
.tc-result{
background:var(--panel);border-radius:4px;
padding:8px 10px;font-size:10px;
color:var(--text2);line-height:1.6;
max-height:180px;overflow-y:auto;
white-space:pre-wrap;word-break:break-word;
}
/* Typing indicator */
.typing{display:flex;align-items:center;gap:4px;padding:8px 12px}
.typing span{
width:5px;height:5px;background:var(--text3);border-radius:50%;
animation:bounce 1.2s infinite;
}
.typing span:nth-child(2){animation-delay:.2s}
.typing span:nth-child(3){animation-delay:.4s}
@keyframes bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
/* ── CHAT INPUT ── */
.chat-input-area{
padding:16px 20px;
background:var(--bg2);
border-top:1px solid var(--border);
flex-shrink:0;
}
.input-wrapper{
display:flex;align-items:flex-end;gap:10px;
background:var(--panel);
border:1px solid var(--border2);
border-radius:10px;
padding:10px 14px;
transition:border-color 0.2s;
}
.input-wrapper:focus-within{border-color:rgba(201,168,76,0.4)}
.input-prefix{color:var(--gold);font-size:14px;font-weight:700;flex-shrink:0;padding-bottom:1px}
#user-input{
flex:1;background:transparent;border:none;outline:none;
color:var(--text);font-family:var(--mono);font-size:13px;
resize:none;line-height:1.5;max-height:120px;
min-height:20px;overflow-y:auto;
}
#user-input::placeholder{color:var(--text3)}
.send-btn{
background:var(--gold);color:#07080d;
border:none;border-radius:6px;
width:32px;height:32px;cursor:pointer;
display:flex;align-items:center;justify-content:center;
font-size:14px;flex-shrink:0;
transition:all 0.15s;font-weight:700;
}
.send-btn:hover{background:#e8c060;transform:scale(1.05)}
.send-btn:disabled{background:var(--border);cursor:not-allowed;transform:none}
.input-footer{
display:flex;justify-content:space-between;
padding:6px 2px 0;font-size:9px;color:var(--text3);
letter-spacing:0.05em;
}
/* ── RIGHT PANEL ── */
.right-panel{
width:280px;flex-shrink:0;
background:var(--bg2);
border-left:1px solid var(--border);
display:flex;flex-direction:column;
overflow:hidden;
}
.rp-header{
padding:12px 16px;
border-bottom:1px solid var(--border);
font-size:9px;letter-spacing:0.2em;text-transform:uppercase;
color:var(--text3);
display:flex;align-items:center;justify-content:space-between;
}
.rp-content{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:12px}
.rp-content::-webkit-scrollbar{width:3px}
.rp-content::-webkit-scrollbar-thumb{background:var(--border2)}
/* Dataset cards */
.dataset-card{
background:var(--panel);border:1px solid var(--border);
border-radius:var(--r);padding:12px;cursor:pointer;
transition:all 0.15s;
}
.dataset-card:hover{border-color:var(--border2);background:var(--panel2)}
.dc-name{font-size:11px;font-weight:600;color:var(--cyan);margin-bottom:4px;word-break:break-all}
.dc-desc{font-size:9px;color:var(--text2);line-height:1.5;margin-bottom:8px}
.dc-tags{display:flex;flex-wrap:wrap;gap:4px}
.dc-tag{
font-size:8px;padding:2px 6px;border-radius:3px;
background:var(--bg2);color:var(--text3);
border:1px solid var(--border);
}
.rp-stat{
display:flex;justify-content:space-between;align-items:center;
padding:6px 0;border-bottom:1px solid var(--border);
font-size:10px;color:var(--text2);
}
.rp-stat:last-child{border-bottom:none}
.rp-stat-val{color:var(--gold);font-weight:600}
/* Inference panel */
.infer-section{margin-top:4px}
.infer-label{font-size:9px;letter-spacing:0.12em;text-transform:uppercase;color:var(--text3);margin-bottom:6px}
.infer-input{
width:100%;background:var(--panel);border:1px solid var(--border);
border-radius:6px;padding:8px 10px;color:var(--text);
font-family:var(--mono);font-size:11px;resize:none;
outline:none;transition:border-color 0.15s;height:60px;
}
.infer-input:focus{border-color:var(--border2)}
.infer-select{
width:100%;margin-top:6px;background:var(--panel);
border:1px solid var(--border);border-radius:6px;
padding:6px 8px;color:var(--text2);font-family:var(--mono);
font-size:11px;outline:none;cursor:pointer;
}
.infer-btn{
width:100%;margin-top:8px;padding:8px;
background:var(--cyan-dim);color:var(--cyan);
border:1px solid rgba(78,205,196,0.25);border-radius:6px;
font-family:var(--mono);font-size:11px;cursor:pointer;
transition:all 0.15s;letter-spacing:0.05em;
}
.infer-btn:hover{background:rgba(78,205,196,0.15)}
.infer-result{
margin-top:8px;background:var(--bg2);border:1px solid var(--border);
border-radius:6px;padding:8px 10px;font-size:10px;
color:var(--text2);line-height:1.6;min-height:40px;
white-space:pre-wrap;word-break:break-word;
}
/* ── SCROLLBAR GLOBAL ── */
::-webkit-scrollbar{width:4px;height:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
</style>
</head>
<body>
<!-- TOP BAR -->
<div class="topbar">
<div class="topbar-left">
<div class="logo-mark">LEX</div>
<div class="logo-sub">Assistente Jurídico · MCP + Hugging Face</div>
</div>
<div class="topbar-status">
<div class="status-pill"><span class="status-dot dot-live"></span>Claude API</div>
<div class="status-pill"><span class="status-dot dot-hf"></span>HF Hub</div>
<div class="status-pill" style="color:var(--text3)">MCP v1.0</div>
</div>
<div class="topbar-right">
<span class="kbd">⌘K</span>
<span class="kbd">Nova sessão</span>
</div>
</div>
<div class="workspace">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-section">
<div class="sidebar-label">Ferramentas MCP</div>
<div class="tool-item active" data-tool="search_legal_models" onclick="selectTool(this, 'Buscar modelos jurídicos no HF Hub para classificação de texto')">
<div class="tool-icon"></div>
<div class="tool-info">
<div class="tool-name">search_legal_models</div>
<div class="tool-desc">Modelos NLP jurídicos</div>
</div>
</div>
<div class="tool-item" data-tool="explore_legal_dataset" onclick="selectTool(this, 'Explorar o dataset joelniklaus/brazilian_court_decisions e mostrar exemplos de decisões')">
<div class="tool-icon"></div>
<div class="tool-info">
<div class="tool-name">explore_legal_dataset</div>
<div class="tool-desc">Datasets de jurisprudência</div>
</div>
</div>
<div class="tool-item" data-tool="analyze_legal_text" onclick="selectTool(this, 'Analisar o seguinte trecho jurídico com summarization: O réu foi condenado à pena de 8 anos de reclusão em regime fechado pela prática do crime previsto no artigo 157 do Código Penal brasileiro, com incidência das causas de aumento do parágrafo segundo.')">
<div class="tool-icon"></div>
<div class="tool-info">
<div class="tool-name">analyze_legal_text</div>
<div class="tool-desc">Inferência NLP ao vivo</div>
</div>
</div>
<div class="tool-item" data-tool="find_jurisprudence" onclick="selectTool(this, 'Buscar decisões sobre habeas corpus no dataset de jurisprudência brasileira')">
<div class="tool-icon"></div>
<div class="tool-info">
<div class="tool-name">find_jurisprudence</div>
<div class="tool-desc">Busca em decisões</div>
</div>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-label">Sessão atual</div>
<div class="rp-stat"><span>Mensagens</span><span class="rp-stat-val" id="msg-count">0</span></div>
<div class="rp-stat"><span>Tool calls</span><span class="rp-stat-val" id="tool-count">0</span></div>
<div class="rp-stat"><span>Modelo</span><span class="rp-stat-val">sonnet-4</span></div>
</div>
<div class="sidebar-footer">
<div class="hf-badge">🤗 Hugging Face Hub</div>
<div>4 ferramentas MCP ativas</div>
<div>Modelos jurídicos · PT/EN</div>
<div style="margin-top:8px;color:var(--text3)">FastMCP · Python 3.10+</div>
</div>
</div>
<!-- CHAT -->
<div class="chat-area">
<div class="chat-messages" id="chat-messages">
<div class="welcome" id="welcome-screen">
<div class="welcome-logo">LEX</div>
<div class="welcome-title">Assistente Jurídico com IA</div>
<div class="welcome-sub">Powered by Claude + MCP + Hugging Face Hub. Busco modelos, analiso textos legais e pesquiso jurisprudência em tempo real.</div>
<div class="suggestion-grid">
<div class="suggestion" onclick="sendSuggestion(this.querySelector('.s-text').textContent)">
<span class="s-icon"></span>
<span class="s-text">Quais modelos de NLP para direito brasileiro estão disponíveis no HF?</span>
</div>
<div class="suggestion" onclick="sendSuggestion(this.querySelector('.s-text').textContent)">
<span class="s-icon"></span>
<span class="s-text">Explore o dataset de decisões judiciais brasileiras e mostre exemplos</span>
</div>
<div class="suggestion" onclick="sendSuggestion(this.querySelector('.s-text').textContent)">
<span class="s-icon"></span>
<span class="s-text">Analise este trecho: "O réu foi condenado por crime de estelionato art. 171 CP"</span>
</div>
<div class="suggestion" onclick="sendSuggestion(this.querySelector('.s-text').textContent)">
<span class="s-icon"></span>
<span class="s-text">Busque decisões sobre habeas corpus na jurisprudência brasileira</span>
</div>
</div>
</div>
</div>
<div class="chat-input-area">
<div class="input-wrapper">
<span class="input-prefix"></span>
<textarea id="user-input" placeholder="Consulte legislação, modelos NLP, jurisprudência..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="send-btn" id="send-btn" onclick="sendMessage()"></button>
</div>
<div class="input-footer">
<span>Enter ↵ para enviar · Shift+Enter para nova linha</span>
<span id="status-text" style="color:var(--text3)">pronto</span>
</div>
</div>
</div>
<!-- RIGHT PANEL -->
<div class="right-panel">
<div class="rp-header">
<span>Datasets Jurídicos</span>
<span style="color:var(--cyan);cursor:pointer" onclick="loadDatasets()"></span>
</div>
<div class="rp-content" id="rp-content">
<div class="dataset-card" onclick="sendSuggestion('Explore o dataset joelniklaus/brazilian_court_decisions')">
<div class="dc-name">brazilian_court_decisions</div>
<div class="dc-desc">Decisões de tribunais brasileiros com outcomes e fundamentos legais.</div>
<div class="dc-tags">
<span class="dc-tag">pt</span><span class="dc-tag">court</span><span class="dc-tag">classification</span>
</div>
</div>
<div class="dataset-card" onclick="sendSuggestion('Explore o dataset lexlms/lex_glue e mostre as tarefas disponíveis')">
<div class="dc-name">lexlms/lex_glue</div>
<div class="dc-desc">Benchmark de NLP jurídico com 6 tarefas cobrindo EU e EUA.</div>
<div class="dc-tags">
<span class="dc-tag">en</span><span class="dc-tag">benchmark</span><span class="dc-tag">NER</span>
</div>
</div>
<div class="dataset-card" onclick="sendSuggestion('Explore o dataset rcmdq/cuad e mostre exemplos de contratos')">
<div class="dc-name">rcmdq/cuad</div>
<div class="dc-desc">510 contratos comerciais anotados com 41 categorias jurídicas.</div>
<div class="dc-tags">
<span class="dc-tag">en</span><span class="dc-tag">contracts</span><span class="dc-tag">QA</span>
</div>
</div>
<div class="dataset-card" onclick="sendSuggestion('Explore o dataset joelniklaus/MultiLegalPile')">
<div class="dc-name">MultiLegalPile</div>
<div class="dc-desc">Corpus multilíngue de textos legais para pré-treinamento.</div>
<div class="dc-tags">
<span class="dc-tag">multilingual</span><span class="dc-tag">legislation</span><span class="dc-tag">pt</span>
</div>
</div>
<!-- Quick inference -->
<div style="border-top:1px solid var(--border);padding-top:12px;margin-top:4px">
<div class="rp-header" style="padding:0 0 10px;border:none">
<span>Inferência Rápida</span>
</div>
<div class="infer-section">
<div class="infer-label">Texto jurídico</div>
<textarea class="infer-input" id="quick-infer-text" placeholder="Cole um trecho de decisão ou contrato..."></textarea>
<select class="infer-select" id="quick-infer-task">
<option value="summarization">Resumo (summarization)</option>
<option value="text-classification">Classificação</option>
<option value="token-classification">NER (entidades)</option>
</select>
<button class="infer-btn" onclick="quickInfer()">▶ Analisar via HF API</button>
<div class="infer-result" id="infer-result">Resultado aparece aqui...</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ── STATE ─────────────────────────────────────────────────────────────────────
let messages = [];
let toolCallCount = 0;
let isLoading = false;
const SYSTEM_PROMPT = `Você é LEX, um assistente jurídico especializado e preciso, com acesso ao Hugging Face Hub via MCP (Model Context Protocol).
Você tem 4 ferramentas MCP disponíveis:
1. search_legal_models(query, language, task, limit) — busca modelos de NLP jurídicos no HF Hub
2. explore_legal_dataset(dataset_id, config, split, n_samples) — explora datasets jurídicos
3. analyze_legal_text(text, task, model_id, context) — roda inferência NLP em texto jurídico
4. find_jurisprudence(keywords, dataset_id, max_results) — busca decisões judiciais por palavras-chave
IMPORTANTE: Você não tem acesso REAL às ferramentas MCP nesta demo. Simule o uso das ferramentas de forma realista e educativa:
- Quando o usuário pedir algo que exigiria uma tool, descreva EXATAMENTE qual tool chamaria, com quais parâmetros
- Mostre um resultado simulado mas plausível (modelos reais do HF Hub, datasets reais, jurisprudência fictícia mas verossímil)
- Seja específico: cite IDs reais de modelos (nlpaueb/legal-bert-base-uncased, joelniklaus/brazilian_court_decisions, etc.)
- Responda SEMPRE em português
- Use formatação markdown: **negrito**, \`código\`, listas
Datasets jurídicos reais no HF Hub para referenciar:
- joelniklaus/brazilian_court_decisions
- lexlms/lex_glue
- rcmdq/cuad (contratos)
- joelniklaus/MultiLegalPile
- legal-mc4 (textos legais multilíngues)
Modelos jurídicos reais no HF Hub:
- nlpaueb/legal-bert-base-uncased (Legal-BERT)
- law-ai/InLegalBERT
- joelniklaus/longformer-base-4096-finetuned-legal (documentos longos)
- Equall/Saul-7B-Instruct (LLM jurídico)
- stjiris/bert-large-portuguese-cased-legal-tsdae-gpl-nli-sts-v0 (português)
Quando simular tool calls, use este formato no início da resposta:
[TOOL: nome_da_ferramenta] params: {...}
[RESULT: resumo do resultado simulado]
Depois responda normalmente em português com base no resultado simulado.`;
// ── RENDER TOOL CALL ──────────────────────────────────────────────────────────
function renderToolCall(toolName, params, result) {
const colors = {
search_legal_models: 'gold',
explore_legal_dataset: 'cyan',
analyze_legal_text: 'purple',
find_jurisprudence: 'green',
};
const color = colors[toolName] || 'gold';
toolCallCount++;
document.getElementById('tool-count').textContent = toolCallCount;
return `<div class="tool-call-card">
<div class="tc-header">
<span>⚡</span>
<span class="tc-name ${color}">${toolName}()</span>
<span class="tc-arrow">→</span>
<span class="tc-status done">✓ concluído</span>
</div>
<div class="tc-params">${params}</div>
<div class="tc-result">${result}</div>
</div>`;
}
// ── PARSE & RENDER MESSAGE ────────────────────────────────────────────────────
function parseAndRenderAssistant(raw) {
let html = '';
let text = raw;
// Extrair tool calls do texto
const toolRegex = /\[TOOL:\s*(\w+)\]\s*params:\s*({[^}]+})\s*\[RESULT:\s*([^\]]+)\]/g;
let match;
let lastIndex = 0;
while ((match = toolRegex.exec(raw)) !== null) {
// texto antes do tool call
const before = raw.slice(lastIndex, match.index).trim();
if (before) html += `<div class="msg-bubble">${mdToHtml(before)}</div>`;
html += renderToolCall(match[1], match[2], match[3]);
lastIndex = match.index + match[0].length;
}
// resto do texto após os tool calls
const after = raw.slice(lastIndex).trim();
if (after) html += `<div class="msg-bubble">${mdToHtml(after)}</div>`;
if (!html) html = `<div class="msg-bubble">${mdToHtml(raw)}</div>`;
return html;
}
function mdToHtml(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/^### (.+)$/gm, '<strong>$1</strong>')
.replace(/^## (.+)$/gm, '<strong>$1</strong>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/^(.+)$/, '<p>$1</p>');
}
// ── APPEND MESSAGE ────────────────────────────────────────────────────────────
function appendMsg(role, contentHtml) {
const welcome = document.getElementById('welcome-screen');
if (welcome) welcome.remove();
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `msg ${role}`;
const avatar = role === 'user'
? '<div class="msg-avatar">U</div>'
: '<div class="msg-avatar">⚖</div>';
div.innerHTML = `${role === 'user' ? '' : avatar}
<div class="msg-body">${contentHtml}</div>
${role === 'user' ? avatar : ''}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
const count = document.querySelectorAll('.msg.user').length;
document.getElementById('msg-count').textContent = count;
}
// ── TYPING INDICATOR ──────────────────────────────────────────────────────────
function showTyping() {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = 'msg assistant';
div.id = 'typing-indicator';
div.innerHTML = `<div class="msg-avatar">⚖</div>
<div class="msg-body"><div class="msg-bubble"><div class="typing"><span></span><span></span><span></span></div></div></div>`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function removeTyping() {
const t = document.getElementById('typing-indicator');
if (t) t.remove();
}
// ── SEND MESSAGE ──────────────────────────────────────────────────────────────
async function sendMessage() {
const input = document.getElementById('user-input');
const text = input.value.trim();
if (!text || isLoading) return;
isLoading = true;
input.value = '';
autoResize(input);
document.getElementById('send-btn').disabled = true;
document.getElementById('status-text').textContent = 'processando...';
// Mostrar mensagem do usuário
appendMsg('user', `<div class="msg-bubble">${text.replace(/</g,'&lt;')}</div>`);
messages.push({ role: 'user', content: text });
showTyping();
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1000,
system: SYSTEM_PROMPT,
messages: messages,
}),
});
const data = await response.json();
removeTyping();
const raw = data.content?.find(b => b.type === 'text')?.text || 'Sem resposta.';
messages.push({ role: 'assistant', content: raw });
appendMsg('assistant', parseAndRenderAssistant(raw));
} catch (err) {
removeTyping();
appendMsg('assistant', `<div class="msg-bubble">Erro ao conectar com a API: <code>${err.message}</code></div>`);
}
isLoading = false;
document.getElementById('send-btn').disabled = false;
document.getElementById('status-text').textContent = 'pronto';
}
// ── QUICK INFERENCE ───────────────────────────────────────────────────────────
async function quickInfer() {
const text = document.getElementById('quick-infer-text').value.trim();
const task = document.getElementById('quick-infer-task').value;
const resultEl = document.getElementById('infer-result');
if (!text) { resultEl.textContent = 'Digite um texto para analisar.'; return; }
resultEl.textContent = '⟳ Chamando Inference API...';
const taskDesc = {
summarization: 'summarization (resumo)',
'text-classification': 'text-classification (área do direito)',
'token-classification': 'token-classification (NER: partes, artigos)',
};
// Simular via Claude API
try {
const resp = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1000,
messages: [{
role: 'user',
content: `Simule o resultado de uma inferência do Hugging Face Inference API com tarefa "${taskDesc[task]}" para o seguinte texto jurídico:\n\n"${text}"\n\nResponda APENAS com o resultado JSON simulado que a API retornaria, sem explicação. Seja realista e específico.`,
}],
}),
});
const d = await resp.json();
const result = d.content?.[0]?.text || 'Erro';
resultEl.textContent = result;
} catch (e) {
resultEl.textContent = 'Erro: ' + e.message;
}
}
// ── HELPERS ───────────────────────────────────────────────────────────────────
function sendSuggestion(text) {
document.getElementById('user-input').value = text;
sendMessage();
}
function selectTool(el, suggestedQuery) {
document.querySelectorAll('.tool-item').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.getElementById('user-input').value = suggestedQuery;
document.getElementById('user-input').focus();
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function loadDatasets() {
// Visual feedback — já estão carregados
const btn = event.target;
btn.style.animation = 'pulse 0.5s ease';
setTimeout(() => btn.style.animation = '', 500);
}
</script>
</body>
</html>