Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>JSONL Conversation Visualizer</title> | |
<style> | |
/* --- Reset and Global Styles --- */ | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
html, | |
body { | |
font-family: | |
system-ui, | |
-apple-system, | |
sans-serif; | |
background: #fafafa; | |
line-height: 1.5; | |
color: #333; | |
/* Prevent the main page from scrolling */ | |
height: 100%; | |
overflow: hidden; | |
} | |
/* --- Main Layout (Flexbox) --- */ | |
.app-container { | |
display: flex; | |
height: 100vh; /* Full viewport height */ | |
} | |
.sidebar { | |
width: 400px; | |
flex-shrink: 0; /* Prevent sidebar from shrinking */ | |
background: #ffffff; | |
border-right: 1px solid #e0e0e0; | |
display: flex; | |
flex-direction: column; | |
overflow-y: auto; /* Allow sidebar to scroll if needed on small screens */ | |
} | |
.main-content { | |
flex-grow: 1; /* Take up remaining space */ | |
display: none; /* Hidden by default, shown by JS */ | |
flex-direction: column; | |
} | |
/* --- Sidebar Components --- */ | |
.input-section { | |
padding: 20px; | |
border-bottom: 1px solid #e0e0e0; | |
} | |
.input-section h1 { | |
margin-bottom: 12px; | |
font-size: 20px; | |
font-weight: 500; | |
} | |
#jsonInput { | |
width: 100%; | |
height: 120px; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 2px; | |
font-family: monospace; | |
font-size: 12px; | |
resize: vertical; | |
} | |
#parseBtn { | |
margin-top: 8px; | |
padding: 8px 16px; | |
background: #333; | |
color: white; | |
border: none; | |
border-radius: 2px; | |
cursor: pointer; | |
font-size: 13px; | |
} | |
#parseBtn:hover { | |
background: #555; | |
} | |
.error { | |
color: #d73a49; | |
margin-top: 8px; | |
font-size: 13px; | |
display: none; | |
} | |
.header-info { | |
padding: 20px; | |
display: none; /* Hidden by default */ | |
} | |
.header-info h2 { | |
margin-bottom: 12px; | |
font-size: 16px; | |
font-weight: 500; | |
} | |
.situation-info { | |
margin-bottom: 16px; | |
font-size: 13px; | |
} | |
.situation-info p { | |
margin-bottom: 4px; | |
} | |
.personas { | |
display: grid; | |
grid-template-columns: 1fr; /* Stack personas vertically in sidebar */ | |
gap: 16px; | |
font-size: 13px; | |
} | |
.persona { | |
padding: 12px; | |
border: 1px solid #e0e0e0; | |
border-radius: 2px; | |
background: #fdfdfd; | |
} | |
.persona h3 { | |
margin-bottom: 6px; | |
font-size: 14px; | |
font-weight: 500; | |
} | |
.traits { | |
margin: 6px 0; | |
} | |
.trait { | |
display: inline-block; | |
background: #f0f0f0; | |
padding: 1px 6px; | |
margin: 0 4px 4px 0; | |
border-radius: 2px; | |
font-size: 11px; | |
} | |
/* --- Chat Area (Right Panel) --- */ | |
.chat-area { | |
padding: 20px 40px; | |
overflow-y: auto; /* The magic: only this area scrolls */ | |
flex-grow: 1; /* Fills the vertical space */ | |
} | |
.message-group { | |
margin-bottom: 16px; | |
} | |
.message { | |
max-width: 70%; | |
margin-bottom: 6px; | |
padding: 10px 12px; | |
border-radius: 4px; | |
font-size: 14px; | |
} | |
.message.persona1 { | |
background: #e1e1e1; | |
margin-left: auto; | |
} | |
.message.persona2 { | |
background: #f0f0f0; | |
margin-right: auto; | |
} | |
.sender-name { | |
font-weight: 500; | |
font-size: 11px; | |
margin-bottom: 4px; | |
opacity: 0.7; | |
text-transform: uppercase; | |
} | |
/* --- Message Content and Special Elements --- */ | |
.message-content { | |
word-wrap: break-word; | |
} | |
.special-element { | |
margin: 6px 0; | |
padding: 6px 8px; | |
background: rgba(0, 0, 0, 0.03); | |
border-left: 2px solid #ccc; | |
font-size: 12px; | |
font-style: italic; | |
} | |
.image-element { | |
border-left-color: #4caf50; | |
} | |
.video-element { | |
border-left-color: #4caf50; | |
} | |
.audio-element { | |
border-left-color: #ff9800; | |
} | |
.delay-element { | |
border-left-color: #9c27b0; | |
text-align: center; | |
} | |
.gif-element { | |
border-left-color: #03a9f4; | |
} | |
.code { | |
background: #f0f0f0; | |
padding: 1px 4px; | |
border-radius: 2px; | |
font-family: monospace; | |
font-size: 12px; | |
} | |
/* --- Responsive Adjustments --- */ | |
@media (max-width: 768px) { | |
.sidebar { | |
width: 300px; /* Slimmer sidebar on smaller screens */ | |
} | |
.message { | |
max-width: 85%; | |
} | |
.chat-area { | |
padding: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="app-container"> | |
<div class="sidebar"> | |
<div class="input-section"> | |
<h1>JSONL Visualizer</h1> | |
<textarea | |
id="jsonInput" | |
placeholder="Paste your JSONL data here..." | |
></textarea> | |
<button id="parseBtn">Parse</button> | |
<div id="error" class="error"></div> | |
</div> | |
<div class="header-info" id="headerInfo"> | |
<h2>Conversation Details</h2> | |
<div class="situation-info" id="situationInfo"></div> | |
<div class="personas" id="personasInfo"></div> | |
</div> | |
</div> | |
<div class="main-content" id="mainContent"> | |
<div class="chat-area" id="chatArea"></div> | |
</div> | |
</div> | |
<script> | |
document | |
.getElementById("parseBtn") | |
.addEventListener("click", parseConversation); | |
function parseConversation() { | |
const input = document.getElementById("jsonInput").value.trim(); | |
const errorDiv = document.getElementById("error"); | |
const headerInfo = document.getElementById("headerInfo"); | |
const mainContent = document.getElementById("mainContent"); | |
// Reset view on each parse attempt | |
errorDiv.style.display = "none"; | |
headerInfo.style.display = "none"; | |
mainContent.style.display = "none"; | |
if (!input) { | |
errorDiv.textContent = "Input cannot be empty."; | |
errorDiv.style.display = "block"; | |
return; | |
} | |
try { | |
const data = JSON.parse(input); | |
// Show the components on successful parse | |
headerInfo.style.display = "block"; | |
mainContent.style.display = "flex"; // Use flex as it's a flex container | |
renderConversation(data); | |
} catch (e) { | |
errorDiv.textContent = "Error parsing JSON: " + e.message; | |
errorDiv.style.display = "block"; | |
} | |
} | |
function renderConversation(data) { | |
renderHeader(data); | |
renderChat(data.chat_parts, data.experience); | |
} | |
function renderHeader(data) { | |
const situationInfo = document.getElementById("situationInfo"); | |
const personasInfo = document.getElementById("personasInfo"); | |
situationInfo.innerHTML = ` | |
<p><strong>Relationship:</strong> ${data.experience.relationship}</p> | |
<p><strong>Context:</strong> ${data.experience.situation}</p> | |
<p><strong>Topic:</strong> ${data.experience.topic}</p> | |
`; | |
const persona1 = data.experience.persona1; | |
const persona2 = data.experience.persona2; | |
personasInfo.innerHTML = ` | |
<div class="persona"> | |
<h3>${persona1.name} (${persona1.age})</h3> | |
<div class="traits"> | |
${persona1.traits.map((trait) => `<span class="trait">${trait}</span>`).join("")} | |
</div> | |
<p><strong>Background:</strong> ${persona1.background}</p> | |
<p><strong>Style:</strong> ${persona1.chatting_style}</p> | |
</div> | |
<div class="persona"> | |
<h3>${persona2.name} (${persona2.age})</h3> | |
<div class="traits"> | |
${persona2.traits.map((trait) => `<span class="trait">${trait}</span>`).join("")} | |
</div> | |
<p><strong>Background:</strong> ${persona2.background}</p> | |
<p><strong>Style:</strong> ${persona2.chatting_style}</p> | |
</div> | |
`; | |
} | |
function renderChat(chatParts, experience) { | |
const chatArea = document.getElementById("chatArea"); | |
chatArea.innerHTML = ""; | |
chatParts.forEach((part) => { | |
const senderClass = | |
part.sender === experience.persona1.id | |
? "persona1" | |
: "persona2"; | |
const senderName = | |
part.sender === experience.persona1.id | |
? experience.persona1.name | |
: experience.persona2.name; | |
const messageGroup = document.createElement("div"); | |
messageGroup.className = "message-group"; | |
part.messages.forEach((messageContent) => { | |
const messageDiv = document.createElement("div"); | |
messageDiv.className = `message ${senderClass}`; | |
const senderDiv = document.createElement("div"); | |
senderDiv.className = "sender-name"; | |
senderDiv.textContent = senderName; | |
const contentDiv = document.createElement("div"); | |
contentDiv.className = "message-content"; | |
contentDiv.innerHTML = formatMessage(messageContent); | |
messageDiv.appendChild(senderDiv); | |
messageDiv.appendChild(contentDiv); | |
messageGroup.appendChild(messageDiv); | |
}); | |
chatArea.appendChild(messageGroup); | |
}); | |
// Scroll to the bottom of the chat on render | |
chatArea.scrollTop = chatArea.scrollHeight; | |
} | |
function formatMessage(content) { | |
content = content.replace(/</g, "<").replace(/>/g, ">"); // Basic sanitization first | |
content = content.replace( | |
/<image>(.*?)<\/image>/g, | |
'<div class="special-element image-element">π· Image: $1</div>', | |
); | |
content = content.replace( | |
/<video>(.*?)<\/video>/g, | |
'<div class="special-element video-element">π₯ Video: $1</div>', | |
); | |
content = content.replace( | |
/<audio>(.*?)<\/audio>/g, | |
'<div class="special-element audio-element">π Audio: $1</div>', | |
); | |
content = content.replace( | |
/<gif>(.*?)<\/gif>/g, | |
'<div class="special-element gif-element">ποΈ GIF: $1</div>', | |
); | |
content = content.replace( | |
/<delay\s+(?:hours="(\d+)"\s*)?(?:minutes="(\d+)"\s*)?(?:\/)?>/g, | |
function (match, hours, minutes) { | |
let delay = ""; | |
if (hours) delay += hours + "h "; | |
if (minutes) delay += minutes + "m"; | |
return `<div class="special-element delay-element">β±οΈ Delay: ${delay || "unknown"}</div>`; | |
}, | |
); | |
content = content.replace( | |
/<end\/>/g, | |
'<div class="special-element">π End</div>', | |
); | |
content = content.replace( | |
/<code>(.*?)<\/code>/g, | |
'<span class="code">$1</span>', | |
); | |
content = content.replace(/\n/g, "<br>"); | |
return content; | |
} | |
document | |
.getElementById("jsonInput") | |
.addEventListener("keydown", function (e) { | |
if (e.ctrlKey && e.key === "Enter") { | |
parseConversation(); | |
} | |
}); | |
</script> | |
</body> | |
</html> | |