Last active
June 4, 2025 02:54
-
-
Save celsowm/b68a844602ff5fd9915720f2f23d0fbd to your computer and use it in GitHub Desktop.
SimpleCanvasLLM
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="pt-BR"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>SimpleCanvasLLM with History - Chat Style</title> | |
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" /> | |
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
<style> | |
html, | |
body { | |
height: 100%; | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Poppins', Arial, sans-serif; | |
background-color: #f0f2f5; | |
} | |
body { | |
padding: 15px; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.main-layout { | |
flex: 1; | |
display: flex; | |
flex-direction: row; | |
gap: 15px; | |
overflow: hidden; | |
background-color: #fff; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
} | |
.left-panel { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
border-right: 1px solid #e0e0e0; | |
padding: 15px; | |
} | |
#chat-history { | |
flex-grow: 1; | |
overflow-y: auto; | |
padding: 10px; | |
display: flex; | |
flex-direction: column; | |
gap: 15px; | |
} | |
.chat-interaction { | |
display: flex; | |
flex-direction: column; | |
gap: 8px; | |
} | |
.message-bubble { | |
font-size: 0.75em; | |
padding: 10px 14px; | |
border-radius: 18px; | |
max-width: 80%; | |
word-wrap: break-word; | |
line-height: 1.5; | |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); | |
} | |
.user-bubble { | |
background-color: #007bff; | |
color: white; | |
align-self: flex-end; | |
border-bottom-right-radius: 5px; | |
} | |
.user-prompt-content { | |
white-space: pre-wrap; | |
} | |
.ai-bubble { | |
background-color: #e9ecef; | |
color: #343a40; | |
align-self: flex-start; | |
border-bottom-left-radius: 5px; | |
display: flex; | |
flex-direction: column; | |
gap: 8px; | |
} | |
.llm-comment-content { | |
white-space: pre-wrap; | |
min-height: 1.5em; | |
} | |
.load-artifact-button { | |
font-family: 'Poppins', Arial, sans-serif; | |
font-size: 0.5em; | |
padding: 4px 8px; | |
cursor: pointer; | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
display: none; | |
align-self: flex-start; | |
} | |
.load-artifact-button:hover { | |
background-color: #5a6268; | |
} | |
.prompt-input-area { | |
display: flex; | |
gap: 10px; | |
padding-top: 15px; | |
border-top: 1px solid #e0e0e0; | |
align-items: flex-start; | |
} | |
#user-prompt { | |
flex-grow: 1; | |
min-height: 42px; | |
max-height: 150px; | |
padding: 10px; | |
font-size: 14px; | |
box-sizing: border-box; | |
resize: vertical; | |
border: 1px solid #ced4da; | |
border-radius: 6px; | |
line-height: 1.4; | |
} | |
#user-prompt:focus { | |
border-color: #80bdff; | |
outline: 0; | |
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, .25); | |
} | |
button#send { | |
padding: 0 15px; | |
font-size: 14px; | |
cursor: pointer; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 6px; | |
height: 42px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: background-color 0.15s ease-in-out; | |
} | |
button#send:hover { | |
background-color: #0056b3; | |
} | |
button#send:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
.right-panel { | |
flex: 2; | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
padding: 15px; | |
} | |
.section { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
min-height: 0; | |
border: 1px solid #e0e0e0; | |
border-radius: 8px; | |
background: #fff; | |
} | |
.section h3 { | |
margin: 0; | |
padding: 12px 15px; | |
font-size: 0.95em; | |
font-weight: 600; | |
background: #f8f9fa; | |
border-bottom: 1px solid #e0e0e0; | |
border-top-left-radius: 7px; | |
border-top-right-radius: 7px; | |
color: #343a40; | |
} | |
#tui-editor-container { | |
flex: 1; | |
overflow: hidden; | |
padding: 1px; | |
min-height: 200px; | |
} | |
.toastui-editor-defaultUI { | |
border: none !important; | |
} | |
#status { | |
font-weight: 500; | |
padding: 8px 0; | |
text-align: center; | |
font-size: 0.9em; | |
min-height: 1.5em; | |
} | |
#artefato-wrapper { | |
position: relative; | |
} | |
#shimmer-overlay-artefato { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(200, 210, 220, 0.6); | |
z-index: 100; | |
display: none; | |
opacity: 0; | |
overflow: hidden; | |
border-radius: 8px; | |
transition: opacity 0.5s ease-out; | |
} | |
#shimmer-overlay-artefato::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: -100%; | |
width: 75%; | |
height: 100%; | |
background: linear-gradient(to right, | |
transparent 0%, | |
rgba(255, 255, 255, 0.5) 50%, | |
transparent 100%); | |
animation: shimmer-effect-artefato 1.8s infinite linear; | |
} | |
@keyframes shimmer-effect-artefato { | |
0% { | |
transform: translateX(0); | |
} | |
100% { | |
transform: translateX(233%); | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="main-layout"> | |
<div class="left-panel"> | |
<div id="chat-history"></div> | |
<div class="prompt-input-area"> | |
<textarea id="user-prompt" placeholder="Digite seu prompt aqui..."></textarea> | |
<button id="send">Enviar</button> | |
</div> | |
</div> | |
<div class="right-panel"> | |
<div class="section" id="artefato-wrapper"> | |
<div id="shimmer-overlay-artefato"></div> | |
<h3>Artefato</h3> | |
<div id="tui-editor-container"></div> | |
</div> | |
</div> | |
</div> | |
<div id="status"></div> | |
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> | |
<script> | |
// Helper function to unescape JSON string chunks | |
function unescapeJsonChunk(jsonStringChunk) { | |
let s = jsonStringChunk; | |
s = s.replace(/\\\\/g, '\\'); | |
s = s.replace(/\\"/g, '"'); | |
s = s.replace(/\\n/g, '\n'); | |
s = s.replace(/\\r/g, '\r'); | |
s = s.replace(/\\t/g, '\t'); | |
s = s.replace(/\\b/g, '\b'); | |
s = s.replace(/\\f/g, '\f'); | |
s = s.replace(/\\u([0-9A-Fa-f]{4})/g, function (_, hex) { | |
return String.fromCharCode(parseInt(hex, 16)); | |
}); | |
return s; | |
} | |
function stripCodeFence(text) { | |
if (text && /^```[^\n]*\n/.test(text) && text.trim().endsWith("```")) { | |
let linhas = text.split(/\r?\n/); | |
linhas.shift(); | |
if (linhas.length > 0 && linhas[linhas.length - 1].trim() === "```") { | |
linhas.pop(); | |
} | |
return linhas.join("\n"); | |
} | |
return text; | |
} | |
(async function () { | |
const ENDPOINT = "http://localhost:8081/v1/chat/completions"; | |
const MODEL = "gpt-3.5-turbo"; | |
const btnSend = document.getElementById("send"); | |
const elmPromptInput = document.getElementById("user-prompt"); | |
const elmStatus = document.getElementById("status"); | |
const elmChatHistory = document.getElementById("chat-history"); | |
const shimmerOverlayArtefato = document.getElementById('shimmer-overlay-artefato'); | |
let lastArtefactContent = ""; | |
let editor; | |
let currentLLMCommentForHistory = ""; | |
let chatHistoryData = []; | |
let shimmerFadeOutStarted = false; | |
let shimmerIsVisible = false; | |
try { | |
editor = new toastui.Editor({ | |
el: document.querySelector('#tui-editor-container'), | |
height: '100%', | |
initialEditType: 'wysiwyg', | |
previewStyle: 'vertical', | |
initialValue: '', | |
usageStatistics: false | |
}); | |
} catch (e) { | |
console.error("Failed to initialize TUI Editor:", e); | |
document.getElementById('tui-editor-container').textContent = "Erro ao carregar o editor de texto."; | |
} | |
const schema = { | |
type: "object", | |
properties: { | |
is_substantive_content: { | |
type: "boolean", | |
description: "True se o 'artefato' gerado é um conteúdo principal e extenso (como uma redação, código, receita, história longa) que o usuário pode querer referenciar ou modificar em prompts subsequentes. False se o 'artefato' é uma resposta curta, conversacional, uma saudação, uma piada simples, ou uma pergunta que não constitui um documento principal." | |
}, | |
artefato: { | |
type: "string", | |
description: "Conteúdo completo da resposta ao prompt. Se 'is_substantive_content' for true, este campo contém o documento principal. Se 'is_substantive_content' for false, este campo contém a mesma resposta conversacional curta que está no campo 'comentario'." | |
}, | |
comentario: { | |
type: "string", | |
description: "Se 'is_substantive_content' for true, este é um comentário sobre o artefato gerado (descrevendo seu tipo, tema, utilidade). Se 'is_substantive_content' for false, este campo contém a própria resposta conversacional curta ao usuário." | |
} | |
}, | |
required: ["is_substantive_content", "artefato", "comentario"] | |
}; | |
function setStatus(msg) { | |
elmStatus.textContent = msg; | |
if (!msg) { elmStatus.style.color = "transparent"; return; } | |
if (msg.toLowerCase().startsWith("ok")) { elmStatus.style.color = "#28a745"; } | |
else if (msg.toLowerCase().includes("erro")) { elmStatus.style.color = "#dc3545"; } | |
else { elmStatus.style.color = "#6c757d"; } | |
} | |
function showArtefactShimmer() { | |
if (!shimmerOverlayArtefato) return; | |
shimmerOverlayArtefato.style.display = 'block'; | |
shimmerOverlayArtefato.offsetHeight; | |
shimmerOverlayArtefato.style.opacity = '1'; | |
shimmerIsVisible = true; | |
shimmerFadeOutStarted = false; | |
} | |
function startArtefactShimmerFadeOut() { | |
if (!shimmerOverlayArtefato || shimmerFadeOutStarted || !shimmerIsVisible) return; | |
shimmerFadeOutStarted = true; | |
shimmerOverlayArtefato.style.opacity = '0'; | |
const handleTransitionEnd = () => { | |
if (shimmerOverlayArtefato.style.opacity === '0') { | |
shimmerOverlayArtefato.style.display = 'none'; | |
shimmerIsVisible = false; | |
} | |
shimmerOverlayArtefato.removeEventListener('transitionend', handleTransitionEnd); | |
}; | |
shimmerOverlayArtefato.addEventListener('transitionend', handleTransitionEnd); | |
} | |
// *** MODIFIED FUNCTION: addInteractionToHistory *** | |
function addInteractionToHistory(userPromptText, initialLlmCommentText = "...") { | |
const interactionPairDiv = document.createElement('div'); | |
interactionPairDiv.className = 'chat-interaction'; // This div now holds a pair of bubbles | |
// User Bubble | |
const userBubbleDiv = document.createElement('div'); | |
userBubbleDiv.className = 'message-bubble user-bubble'; | |
const userPromptContentDiv = document.createElement('div'); | |
userPromptContentDiv.className = 'user-prompt-content'; | |
userPromptContentDiv.textContent = userPromptText; | |
userBubbleDiv.appendChild(userPromptContentDiv); | |
interactionPairDiv.appendChild(userBubbleDiv); | |
// AI Bubble | |
const aiBubbleDiv = document.createElement('div'); | |
aiBubbleDiv.className = 'message-bubble ai-bubble'; | |
const llmCommentContentDiv = document.createElement('div'); | |
llmCommentContentDiv.className = 'llm-comment-content'; | |
llmCommentContentDiv.textContent = initialLlmCommentText; | |
aiBubbleDiv.appendChild(llmCommentContentDiv); | |
const loadButton = document.createElement('button'); | |
loadButton.className = 'load-artifact-button'; | |
loadButton.textContent = '⏪ Restaurar'; | |
// loadButton.style.display = 'none'; // Already set by CSS class | |
aiBubbleDiv.appendChild(loadButton); | |
interactionPairDiv.appendChild(aiBubbleDiv); | |
elmChatHistory.appendChild(interactionPairDiv); | |
elmChatHistory.scrollTop = elmChatHistory.scrollHeight; | |
const historyEntry = { | |
userPrompt: userPromptText, | |
llmComment: "", | |
artefactContent: null, | |
isSubstantive: false, | |
commentDisplayElement: llmCommentContentDiv, // Points to the text content div | |
loadButtonElement: loadButton | |
}; | |
chatHistoryData.push(historyEntry); | |
return historyEntry; | |
} | |
// *** MODIFIED FUNCTION: addErrorInteractionToHistory *** | |
function addErrorInteractionToHistory(userPromptText, errorMessage) { | |
const interactionPairDiv = document.createElement('div'); | |
interactionPairDiv.className = 'chat-interaction'; | |
// User Bubble (still show the user's prompt) | |
const userBubbleDiv = document.createElement('div'); | |
userBubbleDiv.className = 'message-bubble user-bubble'; | |
const userPromptContentDiv = document.createElement('div'); | |
userPromptContentDiv.className = 'user-prompt-content'; | |
userPromptContentDiv.textContent = userPromptText; | |
userBubbleDiv.appendChild(userPromptContentDiv); | |
interactionPairDiv.appendChild(userBubbleDiv); | |
// AI Bubble (for the error message) | |
const aiBubbleDiv = document.createElement('div'); | |
aiBubbleDiv.className = 'message-bubble ai-bubble'; | |
const llmCommentContentDiv = document.createElement('div'); | |
llmCommentContentDiv.className = 'llm-comment-content'; | |
llmCommentContentDiv.style.color = 'red'; // Error emphasis | |
llmCommentContentDiv.textContent = errorMessage; | |
aiBubbleDiv.appendChild(llmCommentContentDiv); | |
interactionPairDiv.appendChild(aiBubbleDiv); | |
elmChatHistory.appendChild(interactionPairDiv); | |
elmChatHistory.scrollTop = elmChatHistory.scrollHeight; | |
chatHistoryData.push({ | |
userPrompt: userPromptText, | |
llmComment: `Erro: ${errorMessage}`, | |
artefactContent: null, | |
isSubstantive: false, | |
commentDisplayElement: llmCommentContentDiv, | |
loadButtonElement: null // No load button for errors | |
}); | |
} | |
function resetArtefactDisplay() { | |
if (editor) { | |
editor.setMarkdown(''); | |
} | |
} | |
function updateEditorArtefact(rawMarkdown) { | |
if (!editor) return; | |
let textoDesescaped = unescapeJsonChunk(rawMarkdown); | |
let semFences = stripCodeFence(textoDesescaped); | |
editor.setMarkdown(semFences); | |
} | |
function handleLoadArtifact(historyIndex) { | |
if (historyIndex < 0 || historyIndex >= chatHistoryData.length) { | |
setStatus("Erro: Índice de histórico inválido."); | |
return; | |
} | |
const entry = chatHistoryData[historyIndex]; | |
if (entry && entry.isSubstantive && entry.artefactContent) { | |
if (editor) { | |
editor.setMarkdown(entry.artefactContent); | |
lastArtefactContent = entry.artefactContent; | |
setStatus(`Artefato de "${entry.userPrompt.substring(0, 30)}..." carregado no editor.`); | |
elmPromptInput.focus(); | |
} | |
} else { | |
setStatus("Nenhum artefato substantivo para carregar desta entrada."); | |
} | |
} | |
function appendProcessedChunkToBuffer(bufRef, fieldKey, escapedJsonStringChunk, currentHistoryEntry, isCurrentStreamSubstantive) { | |
const unescapedChunk = unescapeJsonChunk(escapedJsonStringChunk); | |
if (fieldKey === "artefato") { | |
bufRef.artefato += unescapedChunk; | |
if (isCurrentStreamSubstantive === true) { | |
updateEditorArtefact(bufRef.artefato); | |
if (bufRef.artefato.length > 0 && !shimmerFadeOutStarted && shimmerIsVisible) { | |
startArtefactShimmerFadeOut(); | |
} | |
} | |
} else if (fieldKey === "comentario") { | |
bufRef.comentario += unescapedChunk; | |
currentLLMCommentForHistory = bufRef.comentario; | |
if (currentHistoryEntry && currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = currentLLMCommentForHistory; | |
elmChatHistory.scrollTop = elmChatHistory.scrollHeight; | |
} | |
} | |
} | |
function handlePartialFieldDataStreaming(bufRef, currentTextBufferRef, isFinalCallContext, parserStateSnapshot, currentHistoryEntry, isCurrentStreamSubstantive) { | |
let fieldKey; | |
let nextFieldSeparatorPattern; | |
if (parserStateSnapshot === 2 /* STATE_IN_ARTEFACTO_EXPECT_COMENTARIO_TRANSITION */) { | |
fieldKey = "artefato"; | |
nextFieldSeparatorPattern = '","comentario":"'; | |
} else if (parserStateSnapshot === 3 /* STATE_IN_COMENTARIO_EXPECT_END_TRANSITION */) { | |
fieldKey = "comentario"; | |
nextFieldSeparatorPattern = '"}'; | |
} else { | |
return false; | |
} | |
const typicalEndPattern = nextFieldSeparatorPattern; | |
const amountToLeaveHeuristic = typicalEndPattern.length + 20; | |
let processNowRaw = ""; | |
if (!isFinalCallContext && currentTextBufferRef.value.length > amountToLeaveHeuristic) { | |
processNowRaw = currentTextBufferRef.value.substring(0, currentTextBufferRef.value.length - amountToLeaveHeuristic); | |
currentTextBufferRef.value = currentTextBufferRef.value.substring(processNowRaw.length); | |
} else if (isFinalCallContext && currentTextBufferRef.value.length > 0) { | |
processNowRaw = currentTextBufferRef.value; | |
currentTextBufferRef.value = ""; | |
} | |
if (processNowRaw.length > 0) { | |
let fragmentToProcess = processNowRaw; | |
if (fragmentToProcess.length > 0) { | |
let trailingBackslashes = 0; | |
for (let i = fragmentToProcess.length - 1; i >= 0; i--) { | |
if (fragmentToProcess[i] === '\\') { | |
trailingBackslashes++; | |
} else { | |
break; | |
} | |
} | |
if (trailingBackslashes % 2 !== 0) { | |
currentTextBufferRef.value = fragmentToProcess.slice(-1) + currentTextBufferRef.value; | |
fragmentToProcess = fragmentToProcess.slice(0, -1); | |
} | |
} | |
if (fragmentToProcess.length > 0) { | |
appendProcessedChunkToBuffer(bufRef, fieldKey, fragmentToProcess, currentHistoryEntry, isCurrentStreamSubstantive); | |
return true; | |
} | |
} | |
return false; | |
} | |
btnSend.addEventListener("click", async () => { | |
const userPromptText = elmPromptInput.value.trim(); | |
if (!userPromptText) { | |
setStatus("Por favor, digite um prompt."); elmPromptInput.focus(); return; | |
} | |
if (!editor) { | |
setStatus("Editor não está pronto."); return; | |
} | |
if (editor) { | |
lastArtefactContent = editor.getMarkdown(); | |
console.log("Último conteúdo do artefato:", lastArtefactContent); | |
} | |
setStatus("Gerando resposta..."); | |
btnSend.disabled = true; | |
showArtefactShimmer(); | |
currentLLMCommentForHistory = ""; | |
let isCurrentStreamSubstantive = null; | |
const currentHistoryEntry = addInteractionToHistory(userPromptText); | |
let systemPromptContent = ` | |
Você é um gerador de objetos JSON. Responda **exatamente** com este único objeto JSON válido, seguindo o schema abaixo: | |
{ | |
"is_substantive_content": <true_ou_false>, | |
"artefato": "<conteúdo completo da resposta ao prompt do usuário.>", | |
"comentario": "<comentário sobre o artefato OU a própria resposta conversacional>" | |
} | |
DESCRIÇÕES DOS CAMPOS DO SCHEMA: | |
- "is_substantive_content" (booleano): ${schema.properties.is_substantive_content.description} | |
- "artefato" (string): ${schema.properties.artefato.description} | |
- "comentario" (string): ${schema.properties.comentario.description} | |
INSTRUÇÕES ADICIONAIS PARA OS CAMPOS: | |
Instruções para o campo "is_substantive_content": | |
- Além da descrição acima, considere: Se o prompt atual é uma ação sobre um ARTEFATO ANTERIOR (ex: traduzir, resumir), o novo "artefato" (a tradução, resumo, etc.) geralmente também será 'substantive', então "is_substantive_content" deverá ser 'true'. | |
Instruções para o campo "artefato": | |
- Se 'is_substantive_content' for 'true', este campo contém o documento principal/extenso (ex: o texto da redação, o código completo). | |
- Se 'is_substantive_content' for 'false', este campo contém a mesma resposta textual curta que está no campo "comentario" (ex: "Olá!", "Entendido.", "A capital da França é Paris."). NÃO deixe este campo vazio. | |
Instruções para o campo "comentario": | |
- **Se 'is_substantive_content' for 'true'**: | |
O comentário deve ser uma breve mensagem para o usuário, descrevendo o "artefato" substantivo, conforme a descrição do schema. Siga este padrão: | |
1. Comece com uma frase como "Aqui está..." ou "Gerado(a)..." ou "Criei...". | |
2. Descreva o tipo de conteúdo gerado no "artefato" (ex: "uma redação", "uma receita", "um poema", "um código Python"). | |
3. Mencione o tema principal do "artefato" (ex: "sobre inteligência artificial", "de bolo de cenoura"). | |
4. Conclua com uma frase indicando que o conteúdo está no editor e pode ser modificado (ex: "Está pronto no editor ao lado.", "Você pode editá-lo conforme necessário."). | |
- **Se 'is_substantive_content' for 'false'**: | |
O campo "comentario" DEVE CONTER a resposta conversacional direta e curta ao prompt do usuário, como indicado na descrição do schema. O campo "artefato" também conterá esta mesma resposta. | |
Exemplos para 'comentario' (e 'artefato') quando 'is_substantive_content' é 'false': | |
Prompt do usuário: "oi, tudo bem?" -> JSON: { "is_substantive_content": false, "artefato": "Olá! Tudo ótimo por aqui, e com você?", "comentario": "Olá! Tudo ótimo por aqui, e com você?" } | |
Prompt do usuário: "Qual a capital da França?" -> JSON: { "is_substantive_content": false, "artefato": "A capital da França é Paris.", "comentario": "A capital da França é Paris." } | |
Prompt do usuário: "Obrigado" -> JSON: { "is_substantive_content": false, "artefato": "De nada! Se precisar de mais alguma coisa, é só chamar. 😊", "comentario": "De nada! Se precisar de mais alguma coisa, é só chamar. 😊" } | |
Regras de streaming do JSON: | |
1) Inicie com '{"is_substantive_content":'. | |
2) Envie 'true' ou 'false' (sem aspas). | |
3) Continue com ',"artefato":"'. | |
4) Envie todo o conteúdo do artefato (com escapes JSON corretos para caracteres como \\\\n, \\\\", \\\\t). | |
5) Continue com '","comentario":"'. | |
6) Envie o comentário (ou a resposta direta, se não substantivo). | |
7) Termine com '}'. | |
Estrutura: {"is_substantive_content":SEU_BOOLEANO,"artefato":"SEU_CONTEUDO_AQUI","comentario":"SEU_COMENTARIO_OU_RESPOSTA_DIRETA_AQUI"} | |
`.trim(); | |
if (lastArtefactContent && lastArtefactContent.trim() !== "") { | |
systemPromptContent += ` | |
--- | |
ARTEFATO ANTERIOR PARA SUA REFERÊNCIA: | |
${lastArtefactContent} | |
--- | |
Considere o "ARTEFATO ANTERIOR" acima ao responder ao prompt atual do usuário. | |
- Se o prompt for uma ação sobre o ARTEFATO ANTERIOR (ex: "traduzir", "resumir"), o novo "artefato" gerado deve ser o resultado dessa ação. "is_substantive_content" provavelmente será 'true'. | |
- Se o prompt for uma nova solicitação, gere um novo "artefato". Decida "is_substantive_content" com base no novo artefato. | |
`.trim(); | |
} | |
systemPromptContent += ` | |
Use as regras de streaming do JSON estritamente. | |
`.trim(); | |
const body = { | |
model: MODEL, | |
messages: [{ role: "system", content: systemPromptContent }, { role: "user", content: userPromptText }], | |
stream: true, temperature: 0.7, response_format: { type: "json_object", schema: schema } | |
}; | |
let rawLLMOutput = ""; | |
const buf = { is_substantive_content: null, artefato: "", comentario: "" }; | |
try { | |
const resp = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); | |
if (!resp.ok) { | |
const errorText = `Erro HTTP: ${resp.status} - ${resp.statusText}`; | |
setStatus(errorText); | |
if (currentHistoryEntry && currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = `Erro: ${errorText}`; | |
currentHistoryEntry.commentDisplayElement.style.color = 'red'; | |
currentHistoryEntry.llmComment = `Erro: ${errorText}`; | |
} else { | |
addErrorInteractionToHistory(userPromptText, `Erro: ${errorText}`); | |
} | |
return; | |
} | |
const reader = resp.body.getReader(); | |
const decoder = new TextDecoder("utf-8"); | |
let currentTextBufferWrapper = { value: "" }; | |
const STATE_EXPECT_SUBSTANTIVE_START = 0; | |
const STATE_IN_SUBSTANTIVE_VALUE_EXPECT_ARTEFACTO_TRANSITION = 1; | |
const STATE_IN_ARTEFACTO_EXPECT_COMENTARIO_TRANSITION = 2; | |
const STATE_IN_COMENTARIO_EXPECT_END_TRANSITION = 3; | |
const STATE_FINISHED = 4; | |
let parserState = STATE_EXPECT_SUBSTANTIVE_START; | |
let streamEndedByAPI = false; | |
function processBuffer(isFinalCall = false) { | |
let stateChangedInLoopOrBufferConsumed; | |
let bufferModifiedThisIteration; | |
do { | |
stateChangedInLoopOrBufferConsumed = false; | |
bufferModifiedThisIteration = false; | |
if (parserState === STATE_EXPECT_SUBSTANTIVE_START) { | |
const substantiveStartPattern = /^\s*\{\s*"is_substantive_content"\s*:\s*/; | |
const match = currentTextBufferWrapper.value.match(substantiveStartPattern); | |
if (match) { | |
currentTextBufferWrapper.value = currentTextBufferWrapper.value.substring(match[0].length); | |
parserState = STATE_IN_SUBSTANTIVE_VALUE_EXPECT_ARTEFACTO_TRANSITION; | |
stateChangedInLoopOrBufferConsumed = true; bufferModifiedThisIteration = true; | |
} else if (isFinalCall && currentTextBufferWrapper.value.trim() !== "" && !currentTextBufferWrapper.value.trim().startsWith("{")) { | |
setStatus("Erro: Dados inesperados no início."); return false; | |
} | |
} else if (parserState === STATE_IN_SUBSTANTIVE_VALUE_EXPECT_ARTEFACTO_TRANSITION) { | |
const artefatoTransitionRegex = /^\s*(true|false)\s*,\s*"artefato"\s*:\s*"/; | |
const match = currentTextBufferWrapper.value.match(artefatoTransitionRegex); | |
if (match) { | |
const boolStr = match[1]; | |
buf.is_substantive_content = (boolStr === "true"); | |
isCurrentStreamSubstantive = buf.is_substantive_content; | |
if (isCurrentStreamSubstantive === true) { | |
resetArtefactDisplay(); | |
} | |
currentTextBufferWrapper.value = currentTextBufferWrapper.value.substring(match[0].length); | |
parserState = STATE_IN_ARTEFACTO_EXPECT_COMENTARIO_TRANSITION; | |
stateChangedInLoopOrBufferConsumed = true; bufferModifiedThisIteration = true; | |
} else if (isFinalCall && (currentTextBufferWrapper.value.trim().startsWith("true") || currentTextBufferWrapper.value.trim().startsWith("false")) && !currentTextBufferWrapper.value.includes(",")) { | |
const boolStr = currentTextBufferWrapper.value.trim(); | |
if (boolStr === "true") buf.is_substantive_content = true; | |
else if (boolStr === "false") buf.is_substantive_content = false; | |
if (buf.is_substantive_content !== null) { | |
isCurrentStreamSubstantive = buf.is_substantive_content; | |
if (isCurrentStreamSubstantive === true) resetArtefactDisplay(); | |
currentTextBufferWrapper.value = ""; | |
} | |
} | |
} else if (parserState === STATE_IN_ARTEFACTO_EXPECT_COMENTARIO_TRANSITION) { | |
const comentarioStartRegex = /"\s*,\s*"comentario"\s*:\s*"/; | |
const match = currentTextBufferWrapper.value.match(comentarioStartRegex); | |
if (match) { | |
const artefatoRawChunk = currentTextBufferWrapper.value.substring(0, match.index); | |
appendProcessedChunkToBuffer(buf, "artefato", artefatoRawChunk, currentHistoryEntry, isCurrentStreamSubstantive); | |
currentTextBufferWrapper.value = currentTextBufferWrapper.value.substring(match.index + match[0].length); | |
parserState = STATE_IN_COMENTARIO_EXPECT_END_TRANSITION; | |
stateChangedInLoopOrBufferConsumed = true; bufferModifiedThisIteration = true; | |
} else { | |
if (handlePartialFieldDataStreaming(buf, currentTextBufferWrapper, isFinalCall, parserState, currentHistoryEntry, isCurrentStreamSubstantive)) { | |
bufferModifiedThisIteration = true; | |
} | |
} | |
} else if (parserState === STATE_IN_COMENTARIO_EXPECT_END_TRANSITION) { | |
const jsonEndRegex = /"\s*\}/; | |
const match = currentTextBufferWrapper.value.match(jsonEndRegex); | |
if (match) { | |
const comentarioRawChunk = currentTextBufferWrapper.value.substring(0, match.index); | |
appendProcessedChunkToBuffer(buf, "comentario", comentarioRawChunk, currentHistoryEntry, isCurrentStreamSubstantive); | |
currentTextBufferWrapper.value = currentTextBufferWrapper.value.substring(match.index + match[0].length); | |
parserState = STATE_FINISHED; | |
stateChangedInLoopOrBufferConsumed = true; bufferModifiedThisIteration = true; | |
} else { | |
if (handlePartialFieldDataStreaming(buf, currentTextBufferWrapper, isFinalCall, parserState, currentHistoryEntry, isCurrentStreamSubstantive)) { | |
bufferModifiedThisIteration = true; | |
} | |
} | |
} | |
} while ((stateChangedInLoopOrBufferConsumed || bufferModifiedThisIteration) && parserState !== STATE_FINISHED); | |
return true; | |
} | |
function finalizeStream(origin) { | |
if (!processBuffer(true)) { | |
const errorMsg = `Erro parsing final (${origin}).`; | |
setStatus(errorMsg); | |
if (currentHistoryEntry && currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = `${errorMsg} Comentário parcial: ${currentLLMCommentForHistory}`; | |
currentHistoryEntry.commentDisplayElement.style.color = 'red'; | |
currentHistoryEntry.llmComment = `${errorMsg} Comentário parcial: ${currentLLMCommentForHistory}`; | |
} else { | |
addErrorInteractionToHistory(userPromptText, `${errorMsg} Comentário parcial: ${currentLLMCommentForHistory}`); | |
} | |
console.error("Raw LLM output on finalize error:", rawLLMOutput, "Buffer:", currentTextBufferWrapper.value, "Parser State:", parserState, "Final Buf:", buf); | |
return; | |
} | |
currentHistoryEntry.llmComment = currentLLMCommentForHistory; | |
if (currentHistoryEntry.commentDisplayElement && currentHistoryEntry.commentDisplayElement.textContent !== currentLLMCommentForHistory) { | |
currentHistoryEntry.commentDisplayElement.textContent = currentLLMCommentForHistory; | |
} | |
if (currentLLMCommentForHistory.trim() === "" && currentHistoryEntry.commentDisplayElement && buf.is_substantive_content !== null) { | |
// Ensure some text content if comment is empty but element exists | |
if (currentHistoryEntry.commentDisplayElement.textContent.trim() === "...") { // Only if it's still the placeholder | |
currentHistoryEntry.commentDisplayElement.textContent = "(Sem comentário/resposta recebido)"; | |
} | |
} | |
currentHistoryEntry.isSubstantive = buf.is_substantive_content; | |
const finalArtefactContent = unescapeJsonChunk(stripCodeFence(buf.artefato || "")); | |
currentHistoryEntry.artefactContent = finalArtefactContent; | |
if (buf.is_substantive_content === true) { | |
lastArtefactContent = finalArtefactContent; | |
if (currentHistoryEntry.loadButtonElement) { | |
currentHistoryEntry.loadButtonElement.style.display = 'inline-block'; | |
const historyIndex = chatHistoryData.indexOf(currentHistoryEntry); | |
currentHistoryEntry.loadButtonElement.onclick = () => handleLoadArtifact(historyIndex); | |
} | |
} else if (buf.is_substantive_content === false) { | |
lastArtefactContent = ""; | |
} else { | |
console.warn("is_substantive_content not determined. Assuming non-substantive for artifact handling."); | |
lastArtefactContent = ""; | |
} | |
let finalStatusMessage = "OK"; | |
if (parserState !== STATE_FINISHED) { | |
finalStatusMessage = `OK (JSON ${currentLLMCommentForHistory ? 'com resposta/comentário' : ''}${buf.is_substantive_content !== null ? ` subst:${buf.is_substantive_content}` : ' subst:indeterm.'} - Fim: ${parserState})`; | |
} | |
if (currentTextBufferWrapper.value.length > 0) { | |
finalStatusMessage += ` - Resto: "${currentTextBufferWrapper.value.substring(0, Math.min(currentTextBufferWrapper.value.length, 20))}..."`; | |
console.warn("Buffer residual em finalizeStream:", currentTextBufferWrapper.value); | |
} | |
setStatus(finalStatusMessage); | |
} | |
elmPromptInput.value = ""; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { if (!streamEndedByAPI) finalizeStream("reader.done"); break; } | |
const decodedChunk = decoder.decode(value, { stream: true }); | |
for (const line of decodedChunk.split("\n")) { | |
if (line.trimStart().startsWith("data:")) { | |
const payload = line.replace(/^data:\s*/, "").trim(); | |
if (payload === "[DONE]") { | |
streamEndedByAPI = true; finalizeStream("[DONE]"); return; | |
} | |
let delta; | |
try { if (payload) delta = JSON.parse(payload).choices?.[0]?.delta; else continue; } | |
catch (e) { console.warn("Ignorando delta JSON inválido:", payload, e); continue; } | |
if (delta?.content) { | |
let newContent = delta.content; | |
if (rawLLMOutput === "" && currentTextBufferWrapper.value === "" && newContent.length > 0 && newContent.charCodeAt(0) === 0xFEFF) { | |
newContent = newContent.substring(1); | |
} | |
rawLLMOutput += delta.content; | |
currentTextBufferWrapper.value += newContent; | |
if (!processBuffer()) { | |
const errorMsg = "Erro parsing stream."; | |
setStatus(errorMsg); | |
if (currentHistoryEntry && currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = `${errorMsg} Resposta/Comentário: ${currentLLMCommentForHistory}. Veja console.`; | |
currentHistoryEntry.commentDisplayElement.style.color = 'red'; | |
currentHistoryEntry.llmComment = `${errorMsg} Resposta/Comentário: ${currentLLMCommentForHistory}. Veja console.`; | |
} else { | |
addErrorInteractionToHistory(userPromptText, `${errorMsg} Resposta/Comentário: ${currentLLMCommentForHistory}. Veja console.`); | |
} | |
console.error("Raw LLM output on stream parse error:", rawLLMOutput, "Buffer:", currentTextBufferWrapper.value); | |
return; | |
} | |
} | |
} | |
} | |
} | |
} catch (err) { | |
const errorMsg = "Erro conexão/rede: " + err.message; | |
setStatus(errorMsg); | |
if (currentHistoryEntry && currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = errorMsg; | |
currentHistoryEntry.commentDisplayElement.style.color = 'red'; | |
currentHistoryEntry.llmComment = errorMsg; | |
} else { | |
addErrorInteractionToHistory(userPromptText, errorMsg); | |
} | |
console.error("Erro no fetch ou parsing:", err); | |
} finally { | |
btnSend.disabled = false; | |
startArtefactShimmerFadeOut(); | |
} | |
}); | |
elmPromptInput.addEventListener('keypress', function (e) { | |
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); btnSend.click(); } | |
}); | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment