marcodsn commited on
Commit
938ca57
Β·
verified Β·
1 Parent(s): bc08836

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +134 -395
index.html CHANGED
@@ -6,264 +6,51 @@
6
  <title>SOC Visualizer</title>
7
  <style>
8
  /* --- Reset and Global Styles --- */
9
- * {
10
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
-
15
- html,
16
- body {
17
- font-family:
18
- system-ui,
19
- -apple-system,
20
- sans-serif;
21
- background: #fafafa;
22
- line-height: 1.5;
23
- color: #333;
24
- /* Prevent the main page from scrolling */
25
- height: 100%;
26
- overflow: hidden;
27
- }
28
-
29
  /* --- Main Layout (Flexbox) --- */
30
- .app-container {
31
- display: flex;
32
- height: 100vh; /* Full viewport height */
33
- }
34
-
35
- .sidebar {
36
- width: 400px;
37
- flex-shrink: 0; /* Prevent sidebar from shrinking */
38
- background: #ffffff;
39
- border-right: 1px solid #e0e0e0;
40
- display: flex;
41
- flex-direction: column;
42
- overflow-y: auto; /* Allow sidebar to scroll if needed on small screens */
43
- }
44
-
45
- .main-content {
46
- flex-grow: 1; /* Take up remaining space */
47
- display: none; /* Hidden by default, shown by JS */
48
- flex-direction: column;
49
- }
50
-
51
  /* --- Sidebar Components --- */
52
- .input-section {
53
- padding: 20px;
54
- border-bottom: 1px solid #e0e0e0;
55
- }
56
-
57
- .input-section h1 {
58
- margin-bottom: 16px;
59
- font-size: 20px;
60
- font-weight: 500;
61
- }
62
-
63
- .input-section h2 {
64
- font-size: 14px;
65
- font-weight: 500;
66
- margin-top: 16px;
67
- margin-bottom: 8px;
68
- color: #555;
69
- }
70
-
71
- .input-group {
72
- margin-bottom: 12px;
73
- }
74
-
75
- .input-group label {
76
- display: block;
77
- font-size: 13px;
78
- margin-bottom: 4px;
79
- }
80
-
81
- .input-group select,
82
- #jsonInput {
83
- width: 100%;
84
- padding: 8px;
85
- border: 1px solid #ccc;
86
- border-radius: 2px;
87
- font-size: 13px;
88
- background-color: #fff;
89
- }
90
-
91
- #jsonInput {
92
- height: 120px;
93
- font-family: monospace;
94
- font-size: 12px;
95
- resize: vertical;
96
- }
97
-
98
- #loadBtn,
99
- #parseBtn {
100
- margin-top: 8px;
101
- padding: 8px 16px;
102
- background: #333;
103
- color: white;
104
- border: none;
105
- border-radius: 2px;
106
- cursor: pointer;
107
- font-size: 13px;
108
- width: 100%;
109
- }
110
-
111
- #loadBtn:hover,
112
- #parseBtn:hover {
113
- background: #555;
114
- }
115
-
116
- #loadBtn:disabled {
117
- background: #ccc;
118
- cursor: not-allowed;
119
- }
120
-
121
- .error {
122
- color: #d73a49;
123
- margin-top: 8px;
124
- font-size: 13px;
125
- display: none;
126
- }
127
-
128
- .header-info {
129
- padding: 20px;
130
- display: none; /* Hidden by default */
131
- }
132
-
133
- .header-info h2 {
134
- margin-bottom: 12px;
135
- font-size: 16px;
136
- font-weight: 500;
137
- }
138
-
139
- .situation-info {
140
- margin-bottom: 16px;
141
- font-size: 13px;
142
- }
143
-
144
- .situation-info p {
145
- margin-bottom: 4px;
146
- }
147
-
148
- .personas {
149
- display: grid;
150
- grid-template-columns: 1fr; /* Stack personas vertically in sidebar */
151
- gap: 16px;
152
- font-size: 13px;
153
- }
154
-
155
- .persona {
156
- padding: 12px;
157
- border: 1px solid #e0e0e0;
158
- border-radius: 2px;
159
- background: #fdfdfd;
160
- }
161
-
162
- .persona h3 {
163
- margin-bottom: 6px;
164
- font-size: 14px;
165
- font-weight: 500;
166
- }
167
-
168
- .traits {
169
- margin: 6px 0;
170
- }
171
-
172
- .trait {
173
- display: inline-block;
174
- background: #f0f0f0;
175
- padding: 1px 6px;
176
- margin: 0 4px 4px 0;
177
- border-radius: 2px;
178
- font-size: 11px;
179
- }
180
-
181
  /* --- Chat Area (Right Panel) --- */
182
- .chat-area {
183
- padding: 20px 40px;
184
- overflow-y: auto; /* The magic: only this area scrolls */
185
- flex-grow: 1; /* Fills the vertical space */
186
- }
187
-
188
- .message-group {
189
- margin-bottom: 16px;
190
- }
191
-
192
- .message {
193
- max-width: 70%;
194
- margin-bottom: 6px;
195
- padding: 10px 12px;
196
- border-radius: 4px;
197
- font-size: 14px;
198
- }
199
-
200
- .message.persona1 {
201
- background: #e1e1e1;
202
- margin-left: auto;
203
- }
204
-
205
- .message.persona2 {
206
- background: #f0f0f0;
207
- margin-right: auto;
208
- }
209
-
210
- .sender-name {
211
- font-weight: 500;
212
- font-size: 11px;
213
- margin-bottom: 4px;
214
- opacity: 0.7;
215
- text-transform: uppercase;
216
- }
217
-
218
  /* --- Message Content and Special Elements --- */
219
- .message-content {
220
- word-wrap: break-word;
221
- }
222
-
223
- .special-element {
224
- margin: 6px 0;
225
- padding: 6px 8px;
226
- background: rgba(0, 0, 0, 0.03);
227
- border-left: 2px solid #ccc;
228
- font-size: 12px;
229
- font-style: italic;
230
- }
231
- .image-element {
232
- border-left-color: #4caf50;
233
- }
234
- .video-element {
235
- border-left-color: #4caf50;
236
- }
237
- .audio-element {
238
- border-left-color: #ff9800;
239
- }
240
- .delay-element {
241
- border-left-color: #9c27b0;
242
- text-align: center;
243
- }
244
- .gif-element {
245
- border-left-color: #03a9f4;
246
- }
247
- .code {
248
- background: #f0f0f0;
249
- padding: 1px 4px;
250
- border-radius: 2px;
251
- font-family: monospace;
252
- font-size: 12px;
253
- }
254
-
255
  /* --- Responsive Adjustments --- */
256
- @media (max-width: 768px) {
257
- .sidebar {
258
- width: 300px; /* Slimmer sidebar on smaller screens */
259
- }
260
- .message {
261
- max-width: 85%;
262
- }
263
- .chat-area {
264
- padding: 20px;
265
- }
266
- }
267
  </style>
268
  </head>
269
  <body>
@@ -271,30 +58,21 @@
271
  <div class="sidebar">
272
  <div class="input-section">
273
  <h1>SOC Visualizer</h1>
274
-
275
  <div class="input-group">
276
  <label for="fileSelector">1. Select a file</label>
277
  <select id="fileSelector">
278
  <option value="">-- Choose a file --</option>
279
  </select>
280
  </div>
281
-
282
  <div class="input-group">
283
  <label for="lineSelector">2. Select a conversation</label>
284
  <select id="lineSelector" disabled></select>
285
  </div>
286
-
287
  <button id="loadBtn" disabled>Load Conversation</button>
288
-
289
  <hr style="margin: 24px 0" />
290
-
291
  <h2>Or Paste Manually</h2>
292
- <textarea
293
- id="jsonInput"
294
- placeholder="Paste a single JSON conversation object here..."
295
- ></textarea>
296
  <button id="parseBtn">Parse Manual Input</button>
297
-
298
  <div id="error" class="error"></div>
299
  </div>
300
  <div class="header-info" id="headerInfo">
@@ -303,24 +81,28 @@
303
  <div class="personas" id="personasInfo"></div>
304
  </div>
305
  </div>
306
-
307
  <div class="main-content" id="mainContent">
308
  <div class="chat-area" id="chatArea"></div>
309
  </div>
310
  </div>
311
 
312
  <script>
313
- // --- VIRTUAL FILE SYSTEM ---
314
- // In a real application, you would fetch these files.
315
- // For this tool, simply add your JSONL file content here.
316
- // Each key is a filename, and the value is the file's content as a string.
317
- // Each line in the string must be a complete, valid JSON object.
318
- const virtualFiles = {
319
- "soc_sample_1.jsonl": `{ "experience": { "persona1": { "id": "p1", "name": "Alex", "age": 28, "traits": ["tech-savvy", "impatient", "witty"], "background": "Software developer who loves sci-fi movies.", "chatting_style": "Uses short sentences and a lot of tech jargon." }, "persona2": { "id": "p2", "name": "Ben", "age": 30, "traits": ["patient", "thoughtful", "curious"], "background": "Librarian who enjoys classic literature.", "chatting_style": "Writes in full paragraphs and asks many questions." }, "relationship": "Friends", "situation": "Discussing a new movie they both saw.", "topic": "Movie review" }, "chat_parts": [ { "sender": "p1", "messages": ["Did you see 'Galaxy Runners'? The CGI was insane."] }, { "sender": "p2", "messages": ["I did! I found the plot to be a bit derivative of older films, though. What did you think of the story itself?"] }, { "sender": "p1", "messages": ["Story? lol who cares. The FTL jumps looked amazing.", "And that scene with the alien armada... <gif>mind_blown.gif</gif>"] }, { "sender": "p2", "messages": ["Haha, fair enough. The visual spectacle was certainly its strong suit. I just wish they had spent more time developing the main character's motivations."] } ] }
320
- { "experience": { "persona1": { "id": "p1", "name": "Chloe", "age": 22, "traits": ["energetic", "loves food", "uses emojis"], "background": "Culinary student exploring new cafes.", "chatting_style": "Very casual, lots of emojis and slang." }, "persona2": { "id": "p2", "name": "David", "age": 24, "traits": ["calm", "methodical", "minimalist"], "background": "Architecture student who prefers quiet places.", "chatting_style": "Concise and to the point." }, "relationship": "Siblings", "situation": "Making plans for the weekend.", "topic": "Weekend plans" }, "chat_parts": [ { "sender": "p1", "messages": ["heyyyy!! what r u up to this weekend? 🀩"] }, { "sender": "p2", "messages": ["Not much. Probably finishing up my model for studio."] }, { "sender": "p1", "messages": ["omg nooo you have to come with me to this new brunch place downtown! πŸ₯žπŸ§‡πŸ₯‘", "the pics look amazing!! <image>brunch_spread.jpg</image>"] }, { "sender": "p2", "messages": ["Hah, looks intense. Maybe. What time?"] }, { "sender": "p1", "messages": ["11am! be there or be square! πŸ˜‰"] } ] }`,
321
- "soc_sample_2.jsonl": `{ "experience": { "persona1": { "id": "p1", "name": "Sarah", "age": 45, "traits": ["organized", "caring", "formal"], "background": "Project manager and mother of two.", "chatting_style": "Proper grammar and punctuation, plans everything." }, "persona2": { "id": "p2", "name": "Tom", "age": 46, "traits": ["forgetful", "easy-going", "humorous"], "background": "Graphic designer who works from home.", "chatting_style": "Casual, uses ellipses, often cracks jokes." }, "relationship": "Married Couple", "situation": "Coordinating grocery shopping.", "topic": "Groceries" }, "chat_parts": [ { "sender": "p1", "messages": ["Hi honey, I'm heading to the store after work. I've shared the updated grocery list with you. Please take a look."] }, { "sender": "p2", "messages": ["Hey! ok will check... pretty sure we still have milk tho?"] }, { "sender": "p1", "messages": ["The kids finished it this morning. Please double-check the list for quantities. I don't want to forget the baking soda for Maya's science project again."] }, { "sender": "p2", "messages": ["oops right! science fair volcano... classic.", "got it. list looks good. grab me some of those fancy chips? πŸ™", "the sea salt and vinegar ones"] } ] }`,
322
- };
323
- // --- END VIRTUAL FILE SYSTEM ---
 
 
 
 
 
324
 
325
  // --- DOM Elements ---
326
  const fileSelector = document.getElementById("fileSelector");
@@ -338,58 +120,74 @@
338
  fileSelector.addEventListener("change", handleFileSelection);
339
  loadBtn.addEventListener("click", loadSelectedConversation);
340
  parseBtn.addEventListener("click", parseManualInput);
341
- jsonInput.addEventListener("keydown", function (e) {
342
- if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
343
- parseManualInput();
344
- }
345
  });
346
 
347
  // --- Functions ---
348
  function initialize() {
349
- // Populate the file selector on page load
350
- Object.keys(virtualFiles).forEach((filename) => {
351
  const option = document.createElement("option");
352
- option.value = filename;
353
- option.textContent = filename;
354
  fileSelector.appendChild(option);
355
  });
356
  }
357
 
358
- function handleFileSelection() {
359
- const selectedFile = fileSelector.value;
360
  lineSelector.innerHTML = ""; // Clear previous options
361
- if (selectedFile) {
362
- const content = virtualFiles[selectedFile];
363
- const lines = content.split("\n").filter((line) => line.trim() !== "");
364
-
365
- lines.forEach((line, index) => {
366
- const option = document.createElement("option");
367
- option.value = index;
368
- // Try to parse to get a topic for a better label
369
- try {
370
- const data = JSON.parse(line);
371
- option.textContent = `Conversation ${index + 1}: ${data.experience.topic}`;
372
- } catch (e) {
373
- option.textContent = `Conversation ${index + 1}`;
374
- }
375
- lineSelector.appendChild(option);
376
- });
377
-
378
- lineSelector.disabled = false;
379
- loadBtn.disabled = false;
 
 
 
 
 
 
 
 
380
  } else {
381
  lineSelector.disabled = true;
382
  loadBtn.disabled = true;
383
  }
384
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
  function loadSelectedConversation() {
387
- const fileName = fileSelector.value;
388
  const lineIndex = lineSelector.value;
 
389
 
390
- if (fileName && lineIndex !== null) {
391
- const content = virtualFiles[fileName];
392
- const lines = content.split("\n");
393
  const jsonString = lines[lineIndex];
394
  processAndRender(jsonString);
395
  }
@@ -401,29 +199,34 @@
401
  }
402
 
403
  function processAndRender(jsonString) {
404
- // Reset view on each parse attempt
405
- errorDiv.style.display = "none";
406
  headerInfo.style.display = "none";
407
  mainContent.style.display = "none";
408
 
409
  if (!jsonString) {
410
- errorDiv.textContent = "Input cannot be empty.";
411
- errorDiv.style.display = "block";
412
  return;
413
  }
414
 
415
  try {
416
  const data = JSON.parse(jsonString);
417
- // Show the components on successful parse
418
  headerInfo.style.display = "block";
419
- mainContent.style.display = "flex"; // Use flex as it's a flex container
420
  renderConversation(data);
421
  } catch (e) {
422
- errorDiv.textContent = "Error parsing JSON: " + e.message;
423
- errorDiv.style.display = "block";
424
  }
425
  }
426
 
 
 
 
 
 
 
 
 
 
427
  function renderConversation(data) {
428
  renderHeader(data);
429
  renderChat(data.chat_parts, data.experience);
@@ -432,109 +235,45 @@
432
  function renderHeader(data) {
433
  const situationInfo = document.getElementById("situationInfo");
434
  const personasInfo = document.getElementById("personasInfo");
435
-
436
- situationInfo.innerHTML = `
437
- <p><strong>Relationship:</strong> ${data.experience.relationship}</p>
438
- <p><strong>Context:</strong> ${data.experience.situation}</p>
439
- <p><strong>Topic:</strong> ${data.experience.topic}</p>
440
- `;
441
-
442
- const persona1 = data.experience.persona1;
443
- const persona2 = data.experience.persona2;
444
-
445
- personasInfo.innerHTML = `
446
- <div class="persona">
447
- <h3>${persona1.name} (${persona1.age})</h3>
448
- <div class="traits">
449
- ${persona1.traits.map((trait) => `<span class="trait">${trait}</span>`).join("")}
450
- </div>
451
- <p><strong>Background:</strong> ${persona1.background}</p>
452
- <p><strong>Style:</strong> ${persona1.chatting_style}</p>
453
- </div>
454
- <div class="persona">
455
- <h3>${persona2.name} (${persona2.age})</h3>
456
- <div class="traits">
457
- ${persona2.traits.map((trait) => `<span class="trait">${trait}</span>`).join("")}
458
- </div>
459
- <p><strong>Background:</strong> ${persona2.background}</p>
460
- <p><strong>Style:</strong> ${persona2.chatting_style}</p>
461
- </div>
462
- `;
463
  }
464
 
465
  function renderChat(chatParts, experience) {
466
  chatArea.innerHTML = "";
467
-
468
- chatParts.forEach((part) => {
469
- const senderClass =
470
- part.sender === experience.persona1.id
471
- ? "persona1"
472
- : "persona2";
473
- const senderName =
474
- part.sender === experience.persona1.id
475
- ? experience.persona1.name
476
- : experience.persona2.name;
477
-
478
  const messageGroup = document.createElement("div");
479
  messageGroup.className = "message-group";
480
-
481
- part.messages.forEach((messageContent) => {
482
  const messageDiv = document.createElement("div");
483
  messageDiv.className = `message ${senderClass}`;
484
-
485
  const senderDiv = document.createElement("div");
486
  senderDiv.className = "sender-name";
487
  senderDiv.textContent = senderName;
488
-
489
  const contentDiv = document.createElement("div");
490
  contentDiv.className = "message-content";
491
  contentDiv.innerHTML = formatMessage(messageContent);
492
-
493
  messageDiv.appendChild(senderDiv);
494
  messageDiv.appendChild(contentDiv);
495
  messageGroup.appendChild(messageDiv);
496
  });
497
  chatArea.appendChild(messageGroup);
498
  });
499
- // Scroll to the bottom of the chat on render
500
  chatArea.scrollTop = chatArea.scrollHeight;
501
  }
502
 
503
  function formatMessage(content) {
504
- content = content.replace(/</g, "&lt;").replace(/>/g, "&gt;"); // Basic sanitization first
505
- content = content.replace(
506
- /&lt;image&gt;(.*?)&lt;\/image&gt;/g,
507
- '<div class="special-element image-element">πŸ“· Image: $1</div>',
508
- );
509
- content = content.replace(
510
- /&lt;video&gt;(.*?)&lt;\/video&gt;/g,
511
- '<div class="special-element video-element">πŸŽ₯ Video: $1</div>',
512
- );
513
- content = content.replace(
514
- /&lt;audio&gt;(.*?)&lt;\/audio&gt;/g,
515
- '<div class="special-element audio-element">πŸ”Š Audio: $1</div>',
516
- );
517
- content = content.replace(
518
- /&lt;gif&gt;(.*?)&lt;\/gif&gt;/g,
519
- '<div class="special-element gif-element">🎞️ GIF: $1</div>',
520
- );
521
- content = content.replace(
522
- /&lt;delay\s+(?:hours="(\d+)"\s*)?(?:minutes="(\d+)"\s*)?(?:\/)?&gt;/g,
523
- function (match, hours, minutes) {
524
- let delay = "";
525
- if (hours) delay += hours + "h ";
526
- if (minutes) delay += minutes + "m";
527
- return `<div class="special-element delay-element">⏱️ Delay: ${delay || "unknown"}</div>`;
528
- },
529
- );
530
- content = content.replace(
531
- /&lt;end\/&gt;/g,
532
- '<div class="special-element">πŸ”š End</div>',
533
- );
534
- content = content.replace(
535
- /&lt;code&gt;(.*?)&lt;\/code&gt;/g,
536
- '<span class="code">$1</span>',
537
- );
538
  content = content.replace(/\n/g, "<br>");
539
  return content;
540
  }
 
6
  <title>SOC Visualizer</title>
7
  <style>
8
  /* --- Reset and Global Styles --- */
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ html, body { font-family: system-ui, -apple-system, sans-serif; background: #fafafa; line-height: 1.5; color: #333; height: 100%; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  /* --- Main Layout (Flexbox) --- */
12
+ .app-container { display: flex; height: 100vh; }
13
+ .sidebar { width: 400px; flex-shrink: 0; background: #ffffff; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; overflow-y: auto; }
14
+ .main-content { flex-grow: 1; display: none; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  /* --- Sidebar Components --- */
16
+ .input-section { padding: 20px; border-bottom: 1px solid #e0e0e0; }
17
+ .input-section h1 { margin-bottom: 16px; font-size: 20px; font-weight: 500; }
18
+ .input-section h2 { font-size: 14px; font-weight: 500; margin-top: 16px; margin-bottom: 8px; color: #555; }
19
+ .input-group { margin-bottom: 12px; }
20
+ .input-group label { display: block; font-size: 13px; margin-bottom: 4px; }
21
+ .input-group select, #jsonInput { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 2px; font-size: 13px; background-color: #fff; }
22
+ #jsonInput { height: 120px; font-family: monospace; font-size: 12px; resize: vertical; }
23
+ #loadBtn, #parseBtn { margin-top: 8px; padding: 8px 16px; background: #333; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 13px; width: 100%; }
24
+ #loadBtn:hover, #parseBtn:hover { background: #555; }
25
+ #loadBtn:disabled { background: #ccc; cursor: not-allowed; }
26
+ .error { color: #d73a49; margin-top: 8px; font-size: 13px; display: none; }
27
+ .header-info { padding: 20px; display: none; }
28
+ .header-info h2 { margin-bottom: 12px; font-size: 16px; font-weight: 500; }
29
+ .situation-info { margin-bottom: 16px; font-size: 13px; }
30
+ .situation-info p { margin-bottom: 4px; }
31
+ .personas { display: grid; grid-template-columns: 1fr; gap: 16px; font-size: 13px; }
32
+ .persona { padding: 12px; border: 1px solid #e0e0e0; border-radius: 2px; background: #fdfdfd; }
33
+ .persona h3 { margin-bottom: 6px; font-size: 14px; font-weight: 500; }
34
+ .traits { margin: 6px 0; }
35
+ .trait { display: inline-block; background: #f0f0f0; padding: 1px 6px; margin: 0 4px 4px 0; border-radius: 2px; font-size: 11px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  /* --- Chat Area (Right Panel) --- */
37
+ .chat-area { padding: 20px 40px; overflow-y: auto; flex-grow: 1; }
38
+ .message-group { margin-bottom: 16px; }
39
+ .message { max-width: 70%; margin-bottom: 6px; padding: 10px 12px; border-radius: 4px; font-size: 14px; }
40
+ .message.persona1 { background: #e1e1e1; margin-left: auto; }
41
+ .message.persona2 { background: #f0f0f0; margin-right: auto; }
42
+ .sender-name { font-weight: 500; font-size: 11px; margin-bottom: 4px; opacity: 0.7; text-transform: uppercase; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  /* --- Message Content and Special Elements --- */
44
+ .message-content { word-wrap: break-word; }
45
+ .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; }
46
+ .image-element { border-left-color: #4caf50; }
47
+ .video-element { border-left-color: #4caf50; }
48
+ .audio-element { border-left-color: #ff9800; }
49
+ .delay-element { border-left-color: #9c27b0; text-align: center; }
50
+ .gif-element { border-left-color: #03a9f4; }
51
+ .code { background: #f0f0f0; padding: 1px 4px; border-radius: 2px; font-family: monospace; font-size: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  /* --- Responsive Adjustments --- */
53
+ @media (max-width: 768px) { .sidebar { width: 300px; } .message { max-width: 85%; } .chat-area { padding: 20px; } }
 
 
 
 
 
 
 
 
 
 
54
  </style>
55
  </head>
56
  <body>
 
58
  <div class="sidebar">
59
  <div class="input-section">
60
  <h1>SOC Visualizer</h1>
 
61
  <div class="input-group">
62
  <label for="fileSelector">1. Select a file</label>
63
  <select id="fileSelector">
64
  <option value="">-- Choose a file --</option>
65
  </select>
66
  </div>
 
67
  <div class="input-group">
68
  <label for="lineSelector">2. Select a conversation</label>
69
  <select id="lineSelector" disabled></select>
70
  </div>
 
71
  <button id="loadBtn" disabled>Load Conversation</button>
 
72
  <hr style="margin: 24px 0" />
 
73
  <h2>Or Paste Manually</h2>
74
+ <textarea id="jsonInput" placeholder="Paste a single JSON conversation object here..."></textarea>
 
 
 
75
  <button id="parseBtn">Parse Manual Input</button>
 
76
  <div id="error" class="error"></div>
77
  </div>
78
  <div class="header-info" id="headerInfo">
 
81
  <div class="personas" id="personasInfo"></div>
82
  </div>
83
  </div>
 
84
  <div class="main-content" id="mainContent">
85
  <div class="chat-area" id="chatArea"></div>
86
  </div>
87
  </div>
88
 
89
  <script>
90
+ // --- CONFIGURATION ---
91
+ // IMPORTANT: Replace these with the raw URLs of the files in your public Hugging Face dataset repository.
92
+ // To get the raw URL, go to your file on the Hub and click the "raw" button.
93
+ const fileSources = [
94
+ {
95
+ name: "marcodsn/SOC-2508",
96
+ url: "https://huggingface.co/datasets/marcodsn/SOC-2508/resolve/main/data.jsonl"
97
+ },
98
+ // {
99
+ // name: "Your Next File",
100
+ // url: "https://huggingface.co/datasets/your-username/your-dataset-name/raw/main/your_file.jsonl"
101
+ // }
102
+ ];
103
+
104
+ // This object will cache file content after the first fetch
105
+ const fileCache = {};
106
 
107
  // --- DOM Elements ---
108
  const fileSelector = document.getElementById("fileSelector");
 
120
  fileSelector.addEventListener("change", handleFileSelection);
121
  loadBtn.addEventListener("click", loadSelectedConversation);
122
  parseBtn.addEventListener("click", parseManualInput);
123
+ jsonInput.addEventListener("keydown", (e) => {
124
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") parseManualInput();
 
 
125
  });
126
 
127
  // --- Functions ---
128
  function initialize() {
129
+ fileSources.forEach((source) => {
 
130
  const option = document.createElement("option");
131
+ option.value = source.url; // The value is the full URL
132
+ option.textContent = source.name; // The text is the friendly name
133
  fileSelector.appendChild(option);
134
  });
135
  }
136
 
137
+ async function handleFileSelection() {
138
+ const selectedUrl = fileSelector.value;
139
  lineSelector.innerHTML = ""; // Clear previous options
140
+ resetError();
141
+
142
+ if (selectedUrl) {
143
+ try {
144
+ const content = await fetchAndCacheFile(selectedUrl);
145
+ const lines = content.split('\n').filter(line => line.trim() !== "");
146
+
147
+ lineSelector.dataset.lines = JSON.stringify(lines);
148
+
149
+ lines.forEach((line, index) => {
150
+ const option = document.createElement("option");
151
+ option.value = index;
152
+ try {
153
+ const data = JSON.parse(line);
154
+ option.textContent = `Conversation ${index + 1}: ${data.experience.topic}`;
155
+ } catch (e) {
156
+ option.textContent = `Conversation ${index + 1} (unparsable)`;
157
+ }
158
+ lineSelector.appendChild(option);
159
+ });
160
+ lineSelector.disabled = false;
161
+ loadBtn.disabled = false;
162
+ } catch (err) {
163
+ showError(`Failed to load file: ${err.message}`);
164
+ lineSelector.disabled = true;
165
+ loadBtn.disabled = true;
166
+ }
167
  } else {
168
  lineSelector.disabled = true;
169
  loadBtn.disabled = true;
170
  }
171
  }
172
+
173
+ async function fetchAndCacheFile(url) {
174
+ if (fileCache[url]) {
175
+ return fileCache[url];
176
+ }
177
+ const response = await fetch(url);
178
+ if (!response.ok) {
179
+ throw new Error(`HTTP error! status: ${response.status}`);
180
+ }
181
+ const textContent = await response.text();
182
+ fileCache[url] = textContent;
183
+ return textContent;
184
+ }
185
 
186
  function loadSelectedConversation() {
 
187
  const lineIndex = lineSelector.value;
188
+ const lines = JSON.parse(lineSelector.dataset.lines || "[]");
189
 
190
+ if (lines && lineIndex !== null && lines[lineIndex]) {
 
 
191
  const jsonString = lines[lineIndex];
192
  processAndRender(jsonString);
193
  }
 
199
  }
200
 
201
  function processAndRender(jsonString) {
202
+ resetError();
 
203
  headerInfo.style.display = "none";
204
  mainContent.style.display = "none";
205
 
206
  if (!jsonString) {
207
+ showError("Input cannot be empty.");
 
208
  return;
209
  }
210
 
211
  try {
212
  const data = JSON.parse(jsonString);
 
213
  headerInfo.style.display = "block";
214
+ mainContent.style.display = "flex";
215
  renderConversation(data);
216
  } catch (e) {
217
+ showError(`Error parsing JSON: ${e.message}`);
 
218
  }
219
  }
220
 
221
+ function showError(message) {
222
+ errorDiv.textContent = message;
223
+ errorDiv.style.display = "block";
224
+ }
225
+
226
+ function resetError() {
227
+ errorDiv.style.display = "none";
228
+ }
229
+
230
  function renderConversation(data) {
231
  renderHeader(data);
232
  renderChat(data.chat_parts, data.experience);
 
235
  function renderHeader(data) {
236
  const situationInfo = document.getElementById("situationInfo");
237
  const personasInfo = document.getElementById("personasInfo");
238
+ 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>`;
239
+ const p1 = data.experience.persona1, p2 = data.experience.persona2;
240
+ 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>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
  function renderChat(chatParts, experience) {
244
  chatArea.innerHTML = "";
245
+ chatParts.forEach(part => {
246
+ const senderClass = part.sender === experience.persona1.id ? "persona1" : "persona2";
247
+ const senderName = part.sender === experience.persona1.id ? experience.persona1.name : experience.persona2.name;
 
 
 
 
 
 
 
 
248
  const messageGroup = document.createElement("div");
249
  messageGroup.className = "message-group";
250
+ part.messages.forEach(messageContent => {
 
251
  const messageDiv = document.createElement("div");
252
  messageDiv.className = `message ${senderClass}`;
 
253
  const senderDiv = document.createElement("div");
254
  senderDiv.className = "sender-name";
255
  senderDiv.textContent = senderName;
 
256
  const contentDiv = document.createElement("div");
257
  contentDiv.className = "message-content";
258
  contentDiv.innerHTML = formatMessage(messageContent);
 
259
  messageDiv.appendChild(senderDiv);
260
  messageDiv.appendChild(contentDiv);
261
  messageGroup.appendChild(messageDiv);
262
  });
263
  chatArea.appendChild(messageGroup);
264
  });
 
265
  chatArea.scrollTop = chatArea.scrollHeight;
266
  }
267
 
268
  function formatMessage(content) {
269
+ content = content.replace(/</g, "&lt;").replace(/>/g, "&gt;");
270
+ content = content.replace(/&lt;image&gt;(.*?)&lt;\/image&gt;/g, '<div class="special-element image-element">πŸ“· Image: $1</div>');
271
+ content = content.replace(/&lt;video&gt;(.*?)&lt;\/video&gt;/g, '<div class="special-element video-element">πŸŽ₯ Video: $1</div>');
272
+ content = content.replace(/&lt;audio&gt;(.*?)&lt;\/audio&gt;/g, '<div class="special-element audio-element">πŸ”Š Audio: $1</div>');
273
+ content = content.replace(/&lt;gif&gt;(.*?)&lt;\/gif&gt;/g, '<div class="special-element gif-element">🎞️ GIF: $1</div>');
274
+ content = content.replace(/&lt;delay\s+(?:hours="(\d+)"\s*)?(?:minutes="(\d+)"\s*)?(?:\/)?&gt;/g, (match, h, m) => `<div class="special-element delay-element">⏱️ Delay: ${ (h?h+"h ":"")+(m?m+"m":"") || "unknown"}</div>`);
275
+ content = content.replace(/&lt;end\/&gt;/g, '<div class="special-element">πŸ”š End</div>');
276
+ content = content.replace(/&lt;code&gt;(.*?)&lt;\/code&gt;/g, '<span class="code">$1</span>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  content = content.replace(/\n/g, "<br>");
278
  return content;
279
  }