SOC_Visualizer / index.html
marcodsn's picture
Update index.html
9eaf1c8 verified
raw
history blame
14.9 kB
<!doctype html>
<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, "&lt;").replace(/>/g, "&gt;"); // Basic sanitization first
content = content.replace(
/&lt;image&gt;(.*?)&lt;\/image&gt;/g,
'<div class="special-element image-element">πŸ“· Image: $1</div>',
);
content = content.replace(
/&lt;video&gt;(.*?)&lt;\/video&gt;/g,
'<div class="special-element video-element">πŸŽ₯ Video: $1</div>',
);
content = content.replace(
/&lt;audio&gt;(.*?)&lt;\/audio&gt;/g,
'<div class="special-element audio-element">πŸ”Š Audio: $1</div>',
);
content = content.replace(
/&lt;gif&gt;(.*?)&lt;\/gif&gt;/g,
'<div class="special-element gif-element">🎞️ GIF: $1</div>',
);
content = content.replace(
/&lt;delay\s+(?:hours="(\d+)"\s*)?(?:minutes="(\d+)"\s*)?(?:\/)?&gt;/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(
/&lt;end\/&gt;/g,
'<div class="special-element">πŸ”š End</div>',
);
content = content.replace(
/&lt;code&gt;(.*?)&lt;\/code&gt;/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>