File size: 16,764 Bytes
26464c9
9eaf1c8
 
 
 
bc08836
9eaf1c8
 
938ca57
 
9eaf1c8
938ca57
 
 
9eaf1c8
938ca57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9eaf1c8
938ca57
 
 
 
 
 
9eaf1c8
938ca57
 
 
 
 
 
 
 
9eaf1c8
938ca57
9eaf1c8
 
 
 
 
 
bc08836
 
 
 
 
 
 
 
 
 
 
 
 
 
938ca57
bc08836
9eaf1c8
 
 
 
 
 
 
 
 
 
 
 
 
 
938ca57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc08836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938ca57
 
bc08836
 
 
 
938ca57
bc08836
938ca57
 
bc08836
 
 
9eaf1c8
938ca57
 
bc08836
938ca57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc08836
 
 
 
 
938ca57
 
 
 
 
 
 
 
 
 
 
 
 
9eaf1c8
bc08836
 
938ca57
bc08836
938ca57
bc08836
 
 
 
 
 
 
 
 
 
 
938ca57
9eaf1c8
 
 
bc08836
938ca57
9eaf1c8
 
 
 
bc08836
9eaf1c8
938ca57
9eaf1c8
 
938ca57
9eaf1c8
 
 
938ca57
 
 
 
 
 
 
 
 
9eaf1c8
 
 
 
 
 
 
 
938ca57
 
 
9eaf1c8
 
 
 
938ca57
 
 
9eaf1c8
 
938ca57
9eaf1c8
 
 
 
 
 
 
 
 
 
 
 
 
 
7628ebb
 
9eaf1c8
 
 
938ca57
 
 
 
 
7628ebb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938ca57
 
9eaf1c8
 
 
 
 
26464c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<!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>