Created
April 30, 2025 00:12
-
-
Save celsowm/be42978f06e68eb5c52196d01afd6b67 to your computer and use it in GitHub Desktop.
llm chat single page htm (LEVI CHAT)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
<title>Levi Chat</title> | |
<style> | |
body { | |
margin: 0; | |
background: #f7f7f8; | |
font-family: Arial, sans-serif; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 100vh; | |
} | |
.chat-wrapper { | |
width: 600px; | |
height: 80vh; | |
background: #fff; | |
border-radius: 8px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
} | |
/* ===== HEADER ===== */ | |
.chat-header { | |
padding: 8px 16px; | |
border-bottom: 1px solid #e5e5e5; | |
background: #fafafa; | |
display: flex; | |
gap: 8px; | |
align-items: center; | |
} | |
.chat-header select { | |
flex: 1; | |
padding: 6px 10px; | |
border: 1px solid #d1d5db; | |
border-radius: 6px; | |
font-size: 14px; | |
background: #f5f5f7; | |
appearance: none; | |
cursor: pointer; | |
} | |
.input-with-icon { | |
display: flex; | |
align-items: center; | |
background: #f5f5f7; | |
border: 1px solid #d1d5db; | |
border-radius: 6px; | |
overflow: hidden; | |
} | |
.input-with-icon .icon { | |
padding: 6px 8px; | |
font-size: 16px; | |
line-height: 1; | |
} | |
.input-with-icon input { | |
border: none; | |
background: transparent; | |
padding: 6px 10px; | |
font-size: 14px; | |
width: 100px; | |
outline: none; | |
} | |
/* ===== MESSAGES ===== */ | |
.messages { | |
flex: 1; | |
padding: 16px; | |
overflow-y: auto; | |
background: #fff; | |
} | |
.message { | |
margin-bottom: 10px; | |
padding: 10px 15px; | |
border-radius: 20px; | |
max-width: 70%; | |
line-height: 1.4; | |
position: relative; | |
word-wrap: break-word; | |
clear: both; | |
display: inline-block; | |
} | |
.message.user { | |
background-color: #007bff; | |
color: #fff; | |
float: right; | |
text-align: right; | |
} | |
.message.bot { | |
background-color: #f1f1f1; | |
color: #000; | |
float: left; | |
text-align: left; | |
} | |
.message.user .token-count { | |
position: absolute; | |
bottom: 4px; | |
right: 15px; | |
font-size: 0.7em; | |
color: rgba(255,255,255,0.6); | |
} | |
/* ===== INPUT AREA ===== */ | |
.input-container { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
padding: 12px 16px; | |
border-top: 1px solid #e5e5e5; | |
background: #fafafa; | |
} | |
.file-icons { | |
display: flex; | |
align-items: center; | |
gap: 6px; | |
} | |
.file-icon { | |
font-size: 20px; | |
cursor: pointer; | |
color: #6b7280; | |
} | |
.file-name { | |
font-size: 14px; | |
color: #333; | |
max-width: 100px; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
input[type="file"] { | |
display: none; | |
} | |
textarea { | |
flex: 1; | |
resize: none; | |
min-height: 40px; | |
max-height: 120px; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 15px; | |
font-size: 14px; | |
box-sizing: border-box; | |
} | |
/* ===== BUTTONS ===== */ | |
#sendBtn { | |
padding: 10px 16px; | |
border: none; | |
border-radius: 15px; | |
background-color: #28a745; | |
color: #fff; | |
cursor: pointer; | |
font-size: 14px; | |
white-space: nowrap; | |
} | |
#sendBtn:hover { | |
background-color: #218838; | |
} | |
.thinking-btn { | |
background: none; | |
border: none; | |
cursor: pointer; | |
font-size: 20px; | |
color: #6b7280; | |
transition: color 0.2s; | |
} | |
.thinking-btn.active { | |
color: #10b981; | |
} | |
</style> | |
<!-- Font Awesome --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/> | |
<!-- PDF.js --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js"></script> | |
</head> | |
<body> | |
<div class="chat-wrapper"> | |
<!-- HEADER with modelo, IP and porta --> | |
<div class="chat-header"> | |
<select id="modelSelect"></select> | |
<div class="input-with-icon"> | |
<span class="icon">🖥️</span> | |
<input | |
type="text" | |
id="serverIpInput" | |
placeholder="0.0.0.0" | |
value="0.0.0.0" | |
> | |
</div> | |
<div class="input-with-icon"> | |
<span class="icon">🔌</span> | |
<input | |
type="text" | |
id="serverPortInput" | |
placeholder="8081" | |
value="8081" | |
> | |
</div> | |
</div> | |
<!-- MESSAGES --> | |
<div class="messages" id="messages"></div> | |
<!-- INPUT AREA --> | |
<div class="input-container"> | |
<div class="file-icons"> | |
<i class="fa-solid fa-paperclip file-icon" onclick="fileInput.click()"></i> | |
<i class="fa-solid fa-trash file-icon" id="removeFileIcon" style="display:none" onclick="removeFile()"></i> | |
<span id="fileName" class="file-name"></span> | |
<input type="file" id="fileInput" accept=".pdf" onchange="updateFileName()"> | |
</div> | |
<textarea id="userInput" placeholder="Digite seu prompt aqui..."></textarea> | |
<button id="thinkingBtn" class="thinking-btn" title="Thinking OFF"> | |
<i class="fa-solid fa-brain"></i> | |
</button> | |
<button id="sendBtn">Enviar</button> | |
</div> | |
</div> | |
<script> | |
const modelSelect = document.getElementById('modelSelect'); | |
const messagesDiv = document.getElementById('messages'); | |
const textarea = document.getElementById('userInput'); | |
const fileInput = document.getElementById('fileInput'); | |
const fileNameSpan = document.getElementById('fileName'); | |
const removeFileIcon = document.getElementById('removeFileIcon'); | |
const thinkingBtn = document.getElementById('thinkingBtn'); | |
const sendBtn = document.getElementById('sendBtn'); | |
const serverIpInput = document.getElementById('serverIpInput'); | |
const serverPortInput = document.getElementById('serverPortInput'); | |
let thinkingEnabled = false; | |
function getBaseUrl() { | |
const ip = serverIpInput.value.trim() || '0.0.0.0'; | |
const port = serverPortInput.value.trim() || '8081'; | |
return `http://${ip}:${port}`; | |
} | |
function getModelsUrl() { return `${getBaseUrl()}/v1/models`; } | |
function getChatUrl() { return `${getBaseUrl()}/v1/chat/completions`; } | |
function getTokenCountUrl() { return `${getBaseUrl()}/tokenize`; } | |
function fetchModels() { | |
modelSelect.innerHTML = ''; | |
fetch(getModelsUrl()) | |
.then(r => r.json()) | |
.then(data => { | |
if (!data.data) return; | |
data.data.forEach((m, i) => { | |
const opt = document.createElement('option'); | |
opt.value = m.id; | |
opt.textContent = m.id; | |
if (i === 0) opt.selected = true; | |
modelSelect.appendChild(opt); | |
}); | |
}) | |
.catch(console.error); | |
} | |
serverIpInput.addEventListener('change', fetchModels); | |
serverPortInput.addEventListener('change', fetchModels); | |
window.addEventListener('DOMContentLoaded', fetchModels); | |
thinkingBtn.addEventListener('click', () => { | |
thinkingEnabled = !thinkingEnabled; | |
thinkingBtn.classList.toggle('active', thinkingEnabled); | |
thinkingBtn.title = thinkingEnabled ? 'Thinking ON' : 'Thinking OFF'; | |
}); | |
textarea.addEventListener('input', () => { | |
textarea.style.height = 'auto'; | |
textarea.style.height = `${textarea.scrollHeight}px`; | |
}); | |
textarea.addEventListener('keydown', e => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
async function extractTextFromPDF(file) { | |
const pdf = await pdfjsLib.getDocument(URL.createObjectURL(file)).promise; | |
let txt = ''; | |
for (let i = 1; i <= pdf.numPages; i++) { | |
const page = await pdf.getPage(i); | |
const content = await page.getTextContent(); | |
txt += content.items.map(it => it.str).join(' ') + '\n'; | |
} | |
return txt; | |
} | |
async function countTokens(text) { | |
try { | |
const res = await fetch(getTokenCountUrl(), { | |
method: 'POST', | |
headers: {'Content-Type':'application/json'}, | |
body: JSON.stringify({content: text}) | |
}); | |
const js = await res.json(); | |
return js.tokens?.length || 0; | |
} catch { | |
return 0; | |
} | |
} | |
function updateFileName() { | |
const f = fileInput.files[0]; | |
if (f) { | |
fileNameSpan.textContent = f.name; | |
removeFileIcon.style.display = 'inline'; | |
} | |
} | |
function removeFile() { | |
fileInput.value = ''; | |
fileNameSpan.textContent = ''; | |
removeFileIcon.style.display = 'none'; | |
} | |
function scrollToBottom() { | |
messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
} | |
function appendMessage(text, who, tokenCount) { | |
const div = document.createElement('div'); | |
div.className = `message ${who}`; | |
div.innerText = text; | |
if (who === 'user' && typeof tokenCount === 'number') { | |
const span = document.createElement('span'); | |
span.className = 'token-count'; | |
span.innerText = `Tokens: ${tokenCount}`; | |
div.appendChild(span); | |
} | |
messagesDiv.appendChild(div); | |
scrollToBottom(); | |
} | |
async function sendMessage() { | |
const prompt = textarea.value.trim(); | |
const file = fileInput.files[0]; | |
if (!prompt && !file) return; | |
let pdfText = ''; | |
if (file) { | |
pdfText = await extractTextFromPDF(file); | |
} | |
const fullPrompt = (pdfText + '\n' + prompt).trim(); | |
const tokens = await countTokens(fullPrompt); | |
appendMessage(prompt || '[PDF]', 'user', tokens); | |
textarea.value = ''; | |
textarea.style.height = 'auto'; | |
removeFile(); | |
const botDiv = document.createElement('div'); | |
botDiv.className = 'message bot'; | |
botDiv.innerText = 'Carregando...'; | |
messagesDiv.appendChild(botDiv); | |
scrollToBottom(); | |
const body = { | |
model: modelSelect.value, | |
messages: [{role:'user', content: fullPrompt}], | |
stream: true, | |
chat_template_kwargs: { enable_thinking: thinkingEnabled } | |
}; | |
try { | |
const res = await fetch(getChatUrl(), { | |
method: 'POST', | |
headers: {'Content-Type':'application/json'}, | |
body: JSON.stringify(body) | |
}); | |
const reader = res.body.getReader(); | |
const dec = new TextDecoder('utf-8'); | |
let acc = ''; | |
while (true) { | |
const {done,value} = await reader.read(); | |
if (done) break; | |
const chunk = dec.decode(value); | |
for (let line of chunk.split('\n')) { | |
if (!line.startsWith('data:')) continue; | |
const d = line.replace(/^data:\s*/, ''); | |
if (d === '[DONE]') return; | |
try { | |
const json = JSON.parse(d); | |
const c = json.choices[0].delta?.content || ''; | |
acc += c; | |
botDiv.innerText = acc; | |
scrollToBottom(); | |
} catch {} | |
} | |
} | |
} catch(err) { | |
botDiv.innerText = 'Erro: ' + err.message; | |
} | |
} | |
sendBtn.addEventListener('click', sendMessage); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment