marcodsn commited on
Commit
739aaff
Β·
verified Β·
1 Parent(s): 1db7d05

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +371 -17
index.html CHANGED
@@ -1,19 +1,373 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>SOC Visualizer</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { font-family: system-ui, -apple-system, sans-serif; background: #fafafa; line-height: 1.5; color: #333; height: 100%; overflow: hidden; }
10
+
11
+ .app-container { display: flex; height: 100vh; }
12
+ .sidebar { width: 400px; flex-shrink: 0; background: #fff; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; overflow-y: auto; }
13
+ .main-content { flex-grow: 1; display: none; flex-direction: column; overflow: hidden; }
14
+
15
+ .input-section { padding: 20px; border-bottom: 1px solid #e0e0e0; }
16
+ .input-section h1 { margin-bottom: 16px; font-size: 20px; font-weight: 500; }
17
+ .input-section h2 { font-size: 14px; font-weight: 500; margin-top: 16px; margin-bottom: 8px; color: #555; }
18
+ .input-group { margin-bottom: 12px; }
19
+ .input-group label { display: block; font-size: 13px; margin-bottom: 4px; }
20
+ .input-group select, #jsonInput { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 2px; font-size: 13px; background: #fff; }
21
+ #jsonInput { height: 120px; font-family: monospace; font-size: 12px; resize: vertical; }
22
+ #loadBtn, #parseBtn { margin-top: 8px; padding: 8px 16px; background: #333; color: #fff; border: none; border-radius: 2px; cursor: pointer; font-size: 13px; width: 100%; }
23
+ #loadBtn:hover, #parseBtn:hover { background: #555; }
24
+ #loadBtn:disabled { background: #ccc; cursor: not-allowed; }
25
+ .error { color: #d73a49; margin-top: 8px; font-size: 13px; display: none; }
26
+
27
+ .header-info { padding: 20px; display: none; }
28
+ .header-info h2 { margin-bottom: 12px; font-size: 15px; font-weight: 500; }
29
+
30
+ .personas-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
31
+ .persona-chip { padding: 8px 10px; border-radius: 2px; font-size: 13px; font-weight: 500; text-align: center; }
32
+ .persona-chip.p1 { background: #e1e1e1; }
33
+ .persona-chip.p2 { background: #f0f0f0; }
34
+
35
+ .meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
36
+ .meta-item .label { color: #999; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
37
+ .meta-item .value { font-size: 13px; font-weight: 500; }
38
+
39
+ .initial-state-box { font-size: 12px; color: #555; background: #f5f5f5; padding: 8px 10px; border-radius: 2px; margin-bottom: 12px; font-style: italic; }
40
+
41
+ .instant-events-section .ie-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #999; margin-bottom: 6px; }
42
+ .instant-events-section ul { list-style: none; }
43
+ .instant-events-section li { font-size: 12px; color: #555; padding: 3px 0; border-bottom: 1px solid #f0f0f0; }
44
+ .instant-events-section li:last-child { border-bottom: none; }
45
+ .instant-events-section li::before { content: "⚑ "; }
46
+
47
+ .chat-area { padding: 20px 40px; overflow-y: auto; flex-grow: 1; }
48
+
49
+ .state-divider { text-align: center; margin: 20px 0 12px; position: relative; }
50
+ .state-divider::before { content: ""; display: block; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #e0e0e0; }
51
+ .state-divider span { position: relative; background: #fafafa; padding: 0 10px; font-size: 11px; color: #999; }
52
+
53
+ .instant-event-bubble { margin: 8px auto 12px; max-width: 75%; background: #fffbf0; border: 1px solid #ffe082; border-radius: 4px; padding: 8px 12px; font-size: 12px; color: #6d5c00; font-style: italic; text-align: center; }
54
+
55
+ .message-group { margin-bottom: 4px; }
56
+ .message { max-width: 70%; margin-bottom: 4px; padding: 8px 12px; border-radius: 4px; font-size: 14px; }
57
+ .message.p1 { background: #e1e1e1; margin-left: auto; }
58
+ .message.p2 { background: #f0f0f0; margin-right: auto; }
59
+ .sender-name { font-weight: 500; font-size: 10px; margin-bottom: 3px; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; }
60
+ .message-content { word-wrap: break-word; }
61
+ .msg-footer { font-size: 10px; opacity: 0.4; margin-top: 3px; text-align: right; }
62
+
63
+ .audio-msg::before { content: "🎀 "; font-style: normal; }
64
+ .sticker-msg { font-style: italic; color: #888; font-size: 12px; }
65
+ .sticker-msg::before { content: "πŸ–ΌοΈ "; }
66
+
67
+ .exhausted-marker { text-align: center; margin: 20px 0; font-size: 12px; color: #bbb; letter-spacing: 1px; }
68
+
69
+ .summary-section { margin: 20px 0; padding: 12px 14px; background: #f8f8f8; border-radius: 2px; border-left: 3px solid #ddd; font-size: 13px; color: #555; }
70
+ .summary-section .summary-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #aaa; margin-bottom: 6px; }
71
+
72
+ @media (max-width: 768px) { .sidebar { width: 300px; } .message { max-width: 85%; } .chat-area { padding: 20px; } }
73
+ </style>
74
+ </head>
75
+ <body>
76
+ <div class="app-container">
77
+ <div class="sidebar">
78
+ <div class="input-section">
79
+ <h1>SOC Visualizer</h1>
80
+ <div class="input-group">
81
+ <label for="fileSelector">1. Select a file</label>
82
+ <select id="fileSelector">
83
+ <option value="">-- Choose a file --</option>
84
+ </select>
85
+ </div>
86
+ <div class="input-group">
87
+ <label for="lineSelector">2. Select a conversation</label>
88
+ <select id="lineSelector" disabled></select>
89
+ </div>
90
+ <button id="loadBtn" disabled>Load Conversation</button>
91
+ <hr style="margin: 24px 0" />
92
+ <h2>Or Paste Manually</h2>
93
+ <textarea id="jsonInput" placeholder="Paste a single JSON conversation object here..."></textarea>
94
+ <button id="parseBtn">Parse Manual Input</button>
95
+ <div id="error" class="error"></div>
96
+ </div>
97
+ <div class="header-info" id="headerInfo">
98
+ <h2>Conversation Details</h2>
99
+ <div id="personasRow" class="personas-row"></div>
100
+ <div id="metaGrid" class="meta-grid"></div>
101
+ <div id="initialStateDiv" class="initial-state-box"></div>
102
+ <div id="instantEventsDiv" class="instant-events-section"></div>
103
+ </div>
104
+ </div>
105
+ <div class="main-content" id="mainContent">
106
+ <div class="chat-area" id="chatArea"></div>
107
+ </div>
108
+ </div>
109
+
110
+ <script>
111
+ // --- CONFIGURATION ---
112
+ const fileSources = [
113
+ {
114
+ name: "marcodsn/SOC-2602",
115
+ url: "https://huggingface.co/datasets/marcodsn/SOC-2602/resolve/main/data.jsonl"
116
+ },
117
+ ];
118
+
119
+ const fileCache = {};
120
+
121
+ // --- DOM ---
122
+ const fileSelector = document.getElementById("fileSelector");
123
+ const lineSelector = document.getElementById("lineSelector");
124
+ const loadBtn = document.getElementById("loadBtn");
125
+ const parseBtn = document.getElementById("parseBtn");
126
+ const jsonInput = document.getElementById("jsonInput");
127
+ const errorDiv = document.getElementById("error");
128
+ const headerInfo = document.getElementById("headerInfo");
129
+ const mainContent = document.getElementById("mainContent");
130
+ const chatArea = document.getElementById("chatArea");
131
+
132
+ // --- Bootstrap ---
133
+ document.addEventListener("DOMContentLoaded", () => {
134
+ fileSources.forEach(src => {
135
+ const opt = document.createElement("option");
136
+ opt.value = src.url;
137
+ opt.textContent = src.name;
138
+ fileSelector.appendChild(opt);
139
+ });
140
+ });
141
+
142
+ fileSelector.addEventListener("change", handleFileSelection);
143
+ loadBtn.addEventListener("click", loadSelectedConversation);
144
+ parseBtn.addEventListener("click", () => processAndRender(jsonInput.value.trim()));
145
+ jsonInput.addEventListener("keydown", e => {
146
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") processAndRender(jsonInput.value.trim());
147
+ });
148
+
149
+ // --- File loading ---
150
+ async function handleFileSelection() {
151
+ const url = fileSelector.value;
152
+ lineSelector.innerHTML = "";
153
+ resetError();
154
+ if (!url) { lineSelector.disabled = true; loadBtn.disabled = true; return; }
155
+ try {
156
+ const content = await fetchAndCacheFile(url);
157
+ const lines = content.split('\n').filter(l => l.trim());
158
+ lineSelector.dataset.lines = JSON.stringify(lines);
159
+ lines.forEach((line, i) => {
160
+ const opt = document.createElement("option");
161
+ opt.value = i;
162
+ try {
163
+ const data = JSON.parse(line);
164
+ const names = data.meta?.persona_names || ["P1", "P2"];
165
+ const state = data.meta?.experience_meta?.initial_state || "";
166
+ const topic = state.split(';')[0].trim() || `Conversation ${i + 1}`;
167
+ opt.textContent = `#${i + 1} ${names[0]} & ${names[1]}: ${topic}`;
168
+ } catch { opt.textContent = `Conversation ${i + 1} (unparsable)`; }
169
+ lineSelector.appendChild(opt);
170
+ });
171
+ lineSelector.disabled = false;
172
+ loadBtn.disabled = false;
173
+ } catch (err) {
174
+ showError(`Failed to load file: ${err.message}`);
175
+ lineSelector.disabled = true;
176
+ loadBtn.disabled = true;
177
+ }
178
+ }
179
+
180
+ async function fetchAndCacheFile(url) {
181
+ if (fileCache[url]) return fileCache[url];
182
+ const res = await fetch(url);
183
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
184
+ const text = await res.text();
185
+ fileCache[url] = text;
186
+ return text;
187
+ }
188
+
189
+ function loadSelectedConversation() {
190
+ const idx = lineSelector.value;
191
+ const lines = JSON.parse(lineSelector.dataset.lines || "[]");
192
+ if (lines[idx]) processAndRender(lines[idx]);
193
+ }
194
+
195
+ function processAndRender(jsonString) {
196
+ resetError();
197
+ headerInfo.style.display = "none";
198
+ mainContent.style.display = "none";
199
+ if (!jsonString) { showError("Input cannot be empty."); return; }
200
+ try {
201
+ const data = JSON.parse(jsonString);
202
+ headerInfo.style.display = "block";
203
+ mainContent.style.display = "flex";
204
+ renderConversation(data);
205
+ } catch (e) { showError(`Error parsing JSON: ${e.message}`); }
206
+ }
207
+
208
+ function showError(msg) { errorDiv.textContent = msg; errorDiv.style.display = "block"; }
209
+ function resetError() { errorDiv.style.display = "none"; }
210
+
211
+ // --- Turn XML parser ---
212
+ // Uses the browser's HTML parser for robustness with real-world LLM text.
213
+ // Handles <state>, <instant_event>, <message>, <predefined_topics_exhausted/>.
214
+ function parseTurn(turnStr) {
215
+ const exhausted = turnStr.includes("predefined_topics_exhausted");
216
+ // Remove self-closing tag before HTML parsing (HTML5 ignores the slash)
217
+ const cleaned = turnStr.replace(/<predefined_topics_exhausted\s*\/?>/gi, "");
218
+
219
+ const doc = new DOMParser().parseFromString(`<div>${cleaned}</div>`, "text/html");
220
+
221
+ const state = doc.querySelector("state")?.textContent.trim() || "";
222
+ const instantEvent = doc.querySelector("instant_event")?.textContent.trim() || null;
223
+ const messages = [...doc.querySelectorAll("message")].map(m => ({
224
+ time: m.getAttribute("t") || "",
225
+ date: m.getAttribute("d") || "",
226
+ type: m.getAttribute("type") || "text",
227
+ content: m.textContent || ""
228
+ }));
229
+
230
+ return { state, instantEvent, messages, exhausted };
231
+ }
232
+
233
+ // --- Render header (sidebar details) ---
234
+ function renderHeader(data) {
235
+ const meta = data.meta || {};
236
+ const exp = meta.experience_meta || {};
237
+ const names = meta.persona_names || ["Persona 1", "Persona 2"];
238
+
239
+ document.getElementById("personasRow").innerHTML =
240
+ `<div class="persona-chip p1">β—€ ${esc(names[0])}</div>` +
241
+ `<div class="persona-chip p2">${esc(names[1])} β–Ά</div>`;
242
+
243
+ const style = meta.conversation_style || exp.conversation_style || "β€”";
244
+ const cadence = meta.message_cadence || exp.message_cadence || "β€”";
245
+ const nTurns = meta.n_turns ?? (data.turns?.length ?? "β€”");
246
+ const model = (meta.model || exp.model || "β€”").split("/").pop();
247
+ document.getElementById("metaGrid").innerHTML = `
248
+ <div class="meta-item"><div class="label">Style</div><div class="value">${esc(style)}</div></div>
249
+ <div class="meta-item"><div class="label">Cadence</div><div class="value">${esc(cadence)}</div></div>
250
+ <div class="meta-item"><div class="label">Turns</div><div class="value">${nTurns}</div></div>
251
+ <div class="meta-item"><div class="label">Model</div><div class="value" style="font-size:11px">${esc(model)}</div></div>
252
+ `;
253
+
254
+ document.getElementById("initialStateDiv").textContent =
255
+ exp.initial_state || "(no initial state)";
256
+
257
+ const events = exp.instant_events || [];
258
+ const evDiv = document.getElementById("instantEventsDiv");
259
+ evDiv.innerHTML = events.length
260
+ ? `<div class="ie-label">Instant Events</div><ul>${events.map(e => `<li>${esc(e)}</li>`).join("")}</ul>`
261
+ : "";
262
+ }
263
+
264
+ // --- Render chat ---
265
+ function renderChat(data) {
266
+ chatArea.innerHTML = "";
267
+ const names = data.meta?.persona_names || ["Persona 1", "Persona 2"];
268
+ const turns = data.turns || [];
269
+ let lastTopic = null;
270
+
271
+ turns.forEach((turnStr, i) => {
272
+ const isP1 = (i % 2 === 0);
273
+ const cls = isP1 ? "p1" : "p2";
274
+ const name = isP1 ? names[0] : names[1];
275
+ const turn = parseTurn(turnStr);
276
+
277
+ // State-topic divider
278
+ const topic = turn.state.split(";")[0].trim();
279
+ if (topic && topic !== lastTopic) {
280
+ lastTopic = topic;
281
+ const div = document.createElement("div");
282
+ div.className = "state-divider";
283
+ div.innerHTML = `<span>πŸ“ ${esc(topic)}</span>`;
284
+ chatArea.appendChild(div);
285
+ }
286
+
287
+ // Instant-event thought bubble
288
+ if (turn.instantEvent) {
289
+ const bubble = document.createElement("div");
290
+ bubble.className = "instant-event-bubble";
291
+ bubble.textContent = turn.instantEvent;
292
+ chatArea.appendChild(bubble);
293
+ }
294
+
295
+ // Messages
296
+ if (turn.messages.length > 0) {
297
+ const group = document.createElement("div");
298
+ group.className = "message-group";
299
+
300
+ turn.messages.forEach(msg => {
301
+ const msgDiv = document.createElement("div");
302
+ msgDiv.className = `message ${cls}`;
303
+
304
+ const senderDiv = document.createElement("div");
305
+ senderDiv.className = "sender-name";
306
+ senderDiv.textContent = name;
307
+
308
+ const contentDiv = document.createElement("div");
309
+ if (msg.type === "audio") {
310
+ contentDiv.className = "message-content audio-msg";
311
+ // Strip the "[voice note]" prefix added by the model
312
+ contentDiv.textContent = msg.content.replace(/^\[voice note\]\s*/i, "");
313
+ } else if (msg.type === "sticker") {
314
+ contentDiv.className = "message-content sticker-msg";
315
+ contentDiv.textContent = msg.content;
316
+ } else {
317
+ contentDiv.className = "message-content";
318
+ contentDiv.innerHTML = esc(msg.content).replace(/\n/g, "<br>");
319
+ }
320
+
321
+ msgDiv.appendChild(senderDiv);
322
+ msgDiv.appendChild(contentDiv);
323
+
324
+ if (msg.time) {
325
+ const footer = document.createElement("div");
326
+ footer.className = "msg-footer";
327
+ footer.textContent = msg.date ? `${msg.time} Β· ${msg.date}` : msg.time;
328
+ msgDiv.appendChild(footer);
329
+ }
330
+
331
+ group.appendChild(msgDiv);
332
+ });
333
+
334
+ chatArea.appendChild(group);
335
+ }
336
+
337
+ // End-of-conversation marker
338
+ if (turn.exhausted) {
339
+ const marker = document.createElement("div");
340
+ marker.className = "exhausted-marker";
341
+ marker.textContent = "β€” topics exhausted β€”";
342
+ chatArea.appendChild(marker);
343
+ }
344
+ });
345
+
346
+ // Optional final summary
347
+ if (data.final_summary) {
348
+ const summaryDiv = document.createElement("div");
349
+ summaryDiv.className = "summary-section";
350
+ summaryDiv.innerHTML = `<div class="summary-label">Summary</div>${esc(data.final_summary)}`;
351
+ chatArea.appendChild(summaryDiv);
352
+ }
353
+
354
+ chatArea.scrollTop = 0;
355
+ }
356
+
357
+ function renderConversation(data) {
358
+ renderHeader(data);
359
+ renderChat(data);
360
+ }
361
+
362
+ // Minimal HTML escaping (XSS prevention)
363
+ function esc(str) {
364
+ if (!str) return "";
365
+ return String(str)
366
+ .replace(/&/g, "&amp;")
367
+ .replace(/</g, "&lt;")
368
+ .replace(/>/g, "&gt;")
369
+ .replace(/"/g, "&quot;");
370
+ }
371
+ </script>
372
+ </body>
373
  </html>