Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active June 4, 2025 02:54
Show Gist options
  • Save celsowm/b68a844602ff5fd9915720f2f23d0fbd to your computer and use it in GitHub Desktop.
Save celsowm/b68a844602ff5fd9915720f2f23d0fbd to your computer and use it in GitHub Desktop.
SimpleCanvasLLM
<!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