Created
June 28, 2025 21:13
-
-
Save celsowm/5290f65add6b4a3aa8d7ba6082de2b20 to your computer and use it in GitHub Desktop.
Json stream
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, 0.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 type="module"> | |
import { LLMJsonStreamParser } from "./LLMJsonStreamParser.js"; | |
function unescapeJsonChunk(jsonStringChunk) { | |
try { | |
return JSON.parse(`"${jsonStringChunk}"`); | |
} catch (e) { | |
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"); | |
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://10.120.191.11:8000/v1/chat/completions"; | |
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 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 | |
); | |
} | |
function addInteractionToHistory( | |
userPromptText, | |
initialLlmCommentText = "..." | |
) { | |
const interactionPairDiv = document.createElement("div"); | |
interactionPairDiv.className = "chat-interaction"; | |
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); | |
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"; | |
aiBubbleDiv.appendChild(loadButton); | |
interactionPairDiv.appendChild(aiBubbleDiv); | |
elmChatHistory.appendChild(interactionPairDiv); | |
elmChatHistory.scrollTop = elmChatHistory.scrollHeight; | |
const historyEntry = { | |
userPrompt: userPromptText, | |
llmComment: "", | |
artefactContent: null, | |
isSubstantive: false, | |
commentDisplayElement: llmCommentContentDiv, | |
loadButtonElement: loadButton, | |
}; | |
chatHistoryData.push(historyEntry); | |
return historyEntry; | |
} | |
function addErrorInteractionToHistory(userPromptText, errorMessage) { | |
const interactionPairDiv = document.createElement("div"); | |
interactionPairDiv.className = "chat-interaction"; | |
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); | |
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"; | |
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, | |
}); | |
} | |
function updateEditorArtefact(rawMarkdown) { | |
console.log(`[EDITOR] Setting markdown:`, rawMarkdown); | |
if (!editor) { | |
console.error("[EDITOR] Editor not available!"); | |
return; | |
} | |
let semFences = stripCodeFence(rawMarkdown); | |
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." | |
); | |
} | |
} | |
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(); | |
} | |
setStatus("Gerando resposta..."); | |
btnSend.disabled = true; | |
showArtefactShimmer(); | |
const currentHistoryEntry = addInteractionToHistory(userPromptText); | |
const buf = { | |
is_substantive_content: null, | |
artefato: "", | |
comentario: "", | |
}; | |
let currentKey = null; | |
// --- PROMPT DE SISTEMA REFINADO E COMPLETO --- | |
const systemPromptContent = ` | |
Sua única e exclusiva saída deve ser um único objeto JSON que adere estritamente ao schema e às regras abaixo. Não inclua NENHUM texto, explicação ou markdown como \`\`\`json ... \`\`\` fora do objeto JSON. Sua resposta deve começar com '{' e terminar com '}'. | |
{ | |
"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:** | |
1. 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'. | |
1. **Regra para 'artefato':** | |
- Este campo deve conter **APENAS** o conteúdo solicitado (texto, código, poema etc.). | |
- **NUNCA** inclua frases de enquadramento como "Aqui está o texto..." ou "Espero que ajude." neste campo. | |
- Se a resposta for curta e conversacional, o 'artefato' deve ser idêntico ao 'comentario'. | |
2. **Regra para 'comentario':** | |
- **Se 'is_substantive_content' for true:** | |
1. Comece com "Aqui está...", "Gerado(a)..." ou "Criei...". | |
2. Descreva o tipo de conteúdo gerado (ex: "uma redação", "um poema", "um código Python"). | |
3. Mencione o tema principal (ex: "sobre inteligência artificial", "de bolo de cenoura"). | |
4. Conclua 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 deve conter a resposta direta e curta ao prompt do usuário. | |
- 'comentario' e 'artefato' devem ser exatamente iguais. | |
- **Exemplos:** | |
\`\`\`json | |
{ | |
"is_substantive_content": false, | |
"artefato": "Olá! Tudo ótimo por aqui, e com você?", | |
"comentario": "Olá! Tudo ótimo por aqui, e com você?" | |
} | |
\`\`\` | |
\`\`\`json | |
{ | |
"is_substantive_content": false, | |
"artefato": "A capital da França é Paris.", | |
"comentario": "A capital da França é Paris." | |
} | |
\`\`\` | |
\`\`\`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. 😊" | |
} | |
\`\`\` | |
3. **Regra para 'is_substantive_content':** | |
- true se o 'artefato' for um conteúdo principal e elaborado. | |
- false se a resposta for curta, saudação ou pergunta simples. | |
- se o prompt agir sobre artefato anterior (ex: traduzir), também use true. | |
${ | |
lastArtefactContent && lastArtefactContent.trim() !== "" | |
? ` | |
--- | |
ARTEFATO DE CONTEXTO (DA INTERAÇÃO ANTERIOR): | |
${lastArtefactContent} | |
--- | |
Considere o "ARTEFATO DE CONTEXTO" acima ao responder ao prompt atual. Se o prompt for uma ação sobre este artefato, o novo 'artefato' gerado deve ser o resultado dessa ação. | |
` | |
: "" | |
} | |
Lembre-se: APENAS o objeto JSON. Nada mais. | |
`.trim(); | |
// --- FIM DO PROMPT DE SISTEMA --- | |
const body = { | |
messages: [ | |
{ role: "system", content: systemPromptContent }, | |
{ role: "user", content: userPromptText }, | |
], | |
stream: true, | |
temperature: 0.7, | |
response_format: { type: "json_object", schema: schema }, | |
}; | |
try { | |
console.log("[REQUEST] Iniciando requisição para o LLM."); | |
const parser = new LLMJsonStreamParser(); | |
parser.addEventListener("error", (e) => { | |
const errorMsg = `Erro no parser de JSON: ${e.detail.error.message}`; | |
console.error("[PARSER-EVENT] Error:", e.detail); | |
setStatus(errorMsg); | |
if (currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = | |
errorMsg; | |
currentHistoryEntry.commentDisplayElement.style.color = "red"; | |
currentHistoryEntry.llmComment = errorMsg; | |
} else { | |
addErrorInteractionToHistory(userPromptText, errorMsg); | |
} | |
}); | |
parser.addEventListener("key", (e) => { | |
currentKey = e.detail.value; | |
console.log( | |
`%c[PARSER-EVENT] Key: ${currentKey}`, | |
"color: blue;" | |
); | |
}); | |
parser.addEventListener("value", (e) => { | |
console.log(`%c[PARSER-EVENT] Value (Final):`, "color: green;", { | |
path: e.detail.path.join("/"), | |
value: e.detail.value, | |
}); | |
const path = e.detail.path.join("/"); | |
if (path === "is_substantive_content") { | |
buf.is_substantive_content = e.detail.value; | |
} else if (path === "artefato") { | |
buf.artefato = e.detail.value; | |
} else if (path === "comentario") { | |
buf.comentario = e.detail.value; | |
} | |
}); | |
parser.addEventListener("valueChunk", (e) => { | |
const chunk = e.detail.chunk; | |
const unescapedChunk = unescapeJsonChunk(chunk); | |
switch (currentKey) { | |
case "artefato": | |
// Always buffer the artifact content | |
buf.artefato += unescapedChunk; | |
// But only stream to the editor if the content is substantive. | |
// We know this because `is_substantive_content` is parsed before `artefato`. | |
if (buf.is_substantive_content === true) { | |
updateEditorArtefact(buf.artefato); | |
if (!shimmerFadeOutStarted && shimmerIsVisible) { | |
startArtefactShimmerFadeOut(); | |
} | |
} | |
break; | |
case "comentario": | |
buf.comentario += unescapedChunk; | |
if (currentHistoryEntry.commentDisplayElement) { | |
currentHistoryEntry.commentDisplayElement.textContent = | |
buf.comentario; | |
elmChatHistory.scrollTop = elmChatHistory.scrollHeight; | |
} | |
break; | |
} | |
}); | |
parser.addEventListener("done", () => { | |
console.log( | |
"%c[PARSER-EVENT] Done!", | |
"font-weight: bold; color: purple;", | |
"Buffer final:", | |
buf | |
); | |
currentHistoryEntry.llmComment = buf.comentario; | |
if ( | |
currentHistoryEntry.commentDisplayElement.textContent.trim() === | |
"..." | |
) { | |
currentHistoryEntry.commentDisplayElement.textContent = | |
buf.comentario || "(Sem comentário recebido)"; | |
} | |
currentHistoryEntry.isSubstantive = buf.is_substantive_content; | |
const finalArtefactContent = stripCodeFence(buf.artefato || ""); | |
currentHistoryEntry.artefactContent = finalArtefactContent; | |
if (buf.is_substantive_content === true) { | |
lastArtefactContent = finalArtefactContent; | |
updateEditorArtefact(finalArtefactContent); | |
if (currentHistoryEntry.loadButtonElement) { | |
currentHistoryEntry.loadButtonElement.style.display = | |
"inline-block"; | |
const historyIndex = | |
chatHistoryData.indexOf(currentHistoryEntry); | |
currentHistoryEntry.loadButtonElement.onclick = () => | |
handleLoadArtifact(historyIndex); | |
} | |
} else { | |
lastArtefactContent = ""; | |
} | |
setStatus("OK"); | |
btnSend.disabled = false; | |
startArtefactShimmerFadeOut(); | |
}); | |
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}`; | |
throw new Error(errorText); | |
} | |
const reader = resp.body.getReader(); | |
const decoder = new TextDecoder("utf-8"); | |
elmPromptInput.value = ""; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { | |
console.log("[REQUEST] Stream finalizado."); | |
parser.flush(); | |
break; | |
} | |
const decodedChunk = decoder.decode(value, { stream: true }); | |
parser.transform(decodedChunk); | |
} | |
} catch (err) { | |
const errorMsg = "Erro na requisição: " + err.message; | |
console.error("[REQUEST-ERROR]", err); | |
setStatus(errorMsg); | |
if ( | |
currentHistoryEntry.commentDisplayElement.textContent.trim() === | |
"..." | |
) { | |
currentHistoryEntry.commentDisplayElement.textContent = errorMsg; | |
currentHistoryEntry.commentDisplayElement.style.color = "red"; | |
currentHistoryEntry.llmComment = errorMsg; | |
} else { | |
addErrorInteractionToHistory(userPromptText, errorMsg); | |
} | |
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