SOC_Visualizer / index.html
marcodsn's picture
Update index.html
7628ebb verified
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SOC 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; height: 100%; overflow: hidden; }
/* --- Main Layout (Flexbox) --- */
.app-container { display: flex; height: 100vh; }
.sidebar { width: 400px; flex-shrink: 0; background: #ffffff; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; overflow-y: auto; }
.main-content { flex-grow: 1; display: none; flex-direction: column; }
/* --- Sidebar Components --- */
.input-section { padding: 20px; border-bottom: 1px solid #e0e0e0; }
.input-section h1 { margin-bottom: 16px; font-size: 20px; font-weight: 500; }
.input-section h2 { font-size: 14px; font-weight: 500; margin-top: 16px; margin-bottom: 8px; color: #555; }
.input-group { margin-bottom: 12px; }
.input-group label { display: block; font-size: 13px; margin-bottom: 4px; }
.input-group select, #jsonInput { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 2px; font-size: 13px; background-color: #fff; }
#jsonInput { height: 120px; font-family: monospace; font-size: 12px; resize: vertical; }
#loadBtn, #parseBtn { margin-top: 8px; padding: 8px 16px; background: #333; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 13px; width: 100%; }
#loadBtn:hover, #parseBtn:hover { background: #555; }
#loadBtn:disabled { background: #ccc; cursor: not-allowed; }
.error { color: #d73a49; margin-top: 8px; font-size: 13px; display: none; }
.header-info { padding: 20px; display: none; }
.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; 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; flex-grow: 1; }
.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; } .message { max-width: 85%; } .chat-area { padding: 20px; } }
</style>
</head>
<body>
<div class="app-container">
<div class="sidebar">
<div class="input-section">
<h1>SOC Visualizer</h1>
<div class="input-group">
<label for="fileSelector">1. Select a file</label>
<select id="fileSelector">
<option value="">-- Choose a file --</option>
</select>
</div>
<div class="input-group">
<label for="lineSelector">2. Select a conversation</label>
<select id="lineSelector" disabled></select>
</div>
<button id="loadBtn" disabled>Load Conversation</button>
<hr style="margin: 24px 0" />
<h2>Or Paste Manually</h2>
<textarea id="jsonInput" placeholder="Paste a single JSON conversation object here..."></textarea>
<button id="parseBtn">Parse Manual Input</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>
// --- CONFIGURATION ---
// IMPORTANT: Replace these with the raw URLs of the files in your public Hugging Face dataset repository.
// To get the raw URL, go to your file on the Hub and click the "raw" button.
const fileSources = [
{
name: "marcodsn/SOC-2508",
url: "https://huggingface.co/datasets/marcodsn/SOC-2508/resolve/main/data.jsonl"
},
// {
// name: "Your Next File",
// url: "https://huggingface.co/datasets/your-username/your-dataset-name/raw/main/your_file.jsonl"
// }
];
// This object will cache file content after the first fetch
const fileCache = {};
// --- DOM Elements ---
const fileSelector = document.getElementById("fileSelector");
const lineSelector = document.getElementById("lineSelector");
const loadBtn = document.getElementById("loadBtn");
const parseBtn = document.getElementById("parseBtn");
const jsonInput = document.getElementById("jsonInput");
const errorDiv = document.getElementById("error");
const headerInfo = document.getElementById("headerInfo");
const mainContent = document.getElementById("mainContent");
const chatArea = document.getElementById("chatArea");
// --- Event Listeners ---
document.addEventListener("DOMContentLoaded", initialize);
fileSelector.addEventListener("change", handleFileSelection);
loadBtn.addEventListener("click", loadSelectedConversation);
parseBtn.addEventListener("click", parseManualInput);
jsonInput.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") parseManualInput();
});
// --- Functions ---
function initialize() {
fileSources.forEach((source) => {
const option = document.createElement("option");
option.value = source.url; // The value is the full URL
option.textContent = source.name; // The text is the friendly name
fileSelector.appendChild(option);
});
}
async function handleFileSelection() {
const selectedUrl = fileSelector.value;
lineSelector.innerHTML = ""; // Clear previous options
resetError();
if (selectedUrl) {
try {
const content = await fetchAndCacheFile(selectedUrl);
const lines = content.split('\n').filter(line => line.trim() !== "");
lineSelector.dataset.lines = JSON.stringify(lines);
lines.forEach((line, index) => {
const option = document.createElement("option");
option.value = index;
try {
const data = JSON.parse(line);
option.textContent = `Conversation ${index + 1}: ${data.experience.topic}`;
} catch (e) {
option.textContent = `Conversation ${index + 1} (unparsable)`;
}
lineSelector.appendChild(option);
});
lineSelector.disabled = false;
loadBtn.disabled = false;
} catch (err) {
showError(`Failed to load file: ${err.message}`);
lineSelector.disabled = true;
loadBtn.disabled = true;
}
} else {
lineSelector.disabled = true;
loadBtn.disabled = true;
}
}
async function fetchAndCacheFile(url) {
if (fileCache[url]) {
return fileCache[url];
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const textContent = await response.text();
fileCache[url] = textContent;
return textContent;
}
function loadSelectedConversation() {
const lineIndex = lineSelector.value;
const lines = JSON.parse(lineSelector.dataset.lines || "[]");
if (lines && lineIndex !== null && lines[lineIndex]) {
const jsonString = lines[lineIndex];
processAndRender(jsonString);
}
}
function parseManualInput() {
const jsonString = jsonInput.value.trim();
processAndRender(jsonString);
}
function processAndRender(jsonString) {
resetError();
headerInfo.style.display = "none";
mainContent.style.display = "none";
if (!jsonString) {
showError("Input cannot be empty.");
return;
}
try {
const data = JSON.parse(jsonString);
headerInfo.style.display = "block";
mainContent.style.display = "flex";
renderConversation(data);
} catch (e) {
showError(`Error parsing JSON: ${e.message}`);
}
}
function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = "block";
}
function resetError() {
errorDiv.style.display = "none";
}
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 p1 = data.experience.persona1, p2 = data.experience.persona2;
personasInfo.innerHTML = `<div class="persona"><h3>${p1.name} (${p1.age})</h3><div class="traits">${p1.traits.map(t=>`<span class="trait">${t}</span>`).join("")}</div><p><strong>Background:</strong> ${p1.background}</p><p><strong>Style:</strong> ${p1.chatting_style}</p></div><div class="persona"><h3>${p2.name} (${p2.age})</h3><div class="traits">${p2.traits.map(t=>`<span class="trait">${t}</span>`).join("")}</div><p><strong>Background:</strong> ${p2.background}</p><p><strong>Style:</strong> ${p2.chatting_style}</p></div>`;
}
function renderChat(chatParts, experience) {
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);
});
// CHANGE 1: Set scroll to top instead of bottom
chatArea.scrollTop = 0;
}
function formatMessage(content) {
content = content.replace(/</g, "&lt;").replace(/>/g, "&gt;");
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>');
// CHANGE 2: Replaced the old delay regex with a more robust parser that can handle days, hours, and minutes in any order.
content = content.replace(/&lt;delay\s+([^&]*?)\/?&gt;/g, (match, attributes) => {
const parts = {};
const attrRegex = /(\w+)="(\d+)"/g;
let attrMatch;
while ((attrMatch = attrRegex.exec(attributes)) !== null) {
parts[attrMatch[1]] = attrMatch[2]; // e.g., parts['days'] = '1'
}
const d = parts.days ? `${parts.days}d ` : "";
const h = parts.hours ? `${parts.hours}h ` : "";
const m = parts.minutes ? `${parts.minutes}m` : "";
const timeString = (d + h + m).trim() || "unknown";
return `<div class="special-element delay-element">⏱️ Delay: ${timeString}</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;
}
</script>
</body>
</html>