copilot-tracer 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/index.html ADDED
@@ -0,0 +1,407 @@
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>Copilot Tracer</title>
7
+ <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
8
+ <style>
9
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10
+ body { background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', system-ui, monospace; font-size: 13px; }
11
+
12
+ header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
13
+ header h1 { color: #58a6ff; font-size: 16px; font-weight: 700; }
14
+ .badge { background: #21262d; border: 1px solid #30363d; border-radius: 12px; padding: 2px 10px; font-size: 11px; color: #8b949e; }
15
+ .badge.live { border-color: #238636; color: #3fb950; }
16
+ #status-dot { width: 8px; height: 8px; border-radius: 50%; background: #3fb950; display: inline-block; animation: pulse 2s infinite; }
17
+ @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
18
+
19
+ .stats-bar { background: #161b22; border-bottom: 1px solid #30363d; padding: 8px 20px; display: flex; gap: 24px; }
20
+ .stat { display: flex; flex-direction: column; }
21
+ .stat-label { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
22
+ .stat-value { font-size: 15px; font-weight: 700; color: #e6edf3; }
23
+ .stat-value.credits { color: #f0883e; }
24
+ .stat-value.tokens { color: #79c0ff; }
25
+ .stat-value.skills { color: #d2a8ff; }
26
+ .stat-value.mcps { color: #56d364; }
27
+
28
+ .main { display: flex; height: calc(100vh - 100px); }
29
+ .table-panel { flex: 1; overflow: auto; }
30
+ .detail-panel { width: 420px; border-left: 1px solid #30363d; background: #161b22; overflow: auto; display: none; flex-direction: column; }
31
+ .detail-panel.open { display: flex; }
32
+
33
+ table { width: 100%; border-collapse: collapse; }
34
+ thead th { background: #161b22; color: #8b949e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px; text-align: left; position: sticky; top: 0; border-bottom: 1px solid #30363d; cursor: pointer; user-select: none; white-space: nowrap; }
35
+ thead th:hover { color: #c9d1d9; }
36
+ .totals-row td { background: #1c2128; font-weight: 700; color: #e6edf3; padding: 7px 12px; border-bottom: 2px solid #30363d; font-size: 12px; }
37
+ tbody tr { border-bottom: 1px solid #21262d; cursor: pointer; transition: background 0.1s; }
38
+ tbody tr:hover { background: #1c2128; }
39
+ tbody tr.selected { background: #1f3358; }
40
+ tbody tr.running td:first-child { border-left: 3px solid #f0883e; }
41
+ tbody tr.done td:first-child { border-left: 3px solid #3fb950; }
42
+ tbody tr.error td:first-child { border-left: 3px solid #f85149; }
43
+ td { padding: 7px 12px; vertical-align: top; }
44
+ .td-datetime { color: #8b949e; font-size: 11px; white-space: nowrap; }
45
+ .td-prompt { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #e6edf3; }
46
+ .td-credits { color: #f0883e; white-space: nowrap; }
47
+ .td-duration { color: #8b949e; white-space: nowrap; }
48
+ .td-tokens { font-family: monospace; font-size: 11px; }
49
+ .tok-cached { color: #79c0ff; }
50
+ .tok-written { color: #56d364; }
51
+ .tok-reason { color: #d2a8ff; cursor: pointer; text-decoration: underline dotted; }
52
+ .tok-reason:hover { color: #f0883e; }
53
+ .td-count { text-align: center; }
54
+ .pill { display: inline-block; border-radius: 10px; padding: 1px 7px; font-size: 11px; cursor: pointer; }
55
+ .pill-skill { background: #2d1b69; color: #d2a8ff; }
56
+ .pill-agent { background: #0d2942; color: #79c0ff; }
57
+ .pill-mcp { background: #0a3622; color: #56d364; }
58
+ .pill:hover { opacity: 0.8; filter: brightness(1.2); }
59
+
60
+ /* Detail panel */
61
+ .detail-header { padding: 14px 16px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; }
62
+ .detail-header h3 { color: #58a6ff; font-size: 13px; }
63
+ .close-btn { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 18px; line-height: 1; }
64
+ .close-btn:hover { color: #e6edf3; }
65
+ .detail-body { padding: 12px 16px; flex: 1; overflow: auto; }
66
+ .detail-section { margin-bottom: 16px; }
67
+ .detail-section h4 { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
68
+ .detail-box { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 10px; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow: auto; }
69
+ .detail-box.reasoning { color: #d2a8ff; }
70
+ .detail-box.response { color: #56d364; }
71
+ .detail-box.prompt { color: #e6edf3; }
72
+
73
+ /* Call graph */
74
+ .call-tree { font-family: monospace; font-size: 12px; }
75
+ .call-node { padding: 4px 0; }
76
+ .call-node-header { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 4px 6px; border-radius: 4px; }
77
+ .call-node-header:hover { background: #21262d; }
78
+ .call-expand { color: #8b949e; width: 14px; text-align: center; }
79
+ .call-name { font-weight: 600; }
80
+ .call-duration { color: #8b949e; margin-left: auto; font-size: 11px; }
81
+ .call-children { margin-left: 18px; border-left: 1px solid #30363d; padding-left: 8px; }
82
+ .call-io { background: #0d1117; border-radius: 4px; padding: 6px 8px; margin: 4px 0 4px 20px; font-size: 11px; color: #8b949e; }
83
+ .call-io .label { color: #58a6ff; margin-right: 4px; }
84
+ .call-io.error { color: #f85149; }
85
+
86
+ /* Token detail modal */
87
+ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; }
88
+ .modal-overlay.open { display: flex; }
89
+ .modal { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; width: 560px; max-height: 80vh; overflow: auto; }
90
+ .modal h3 { color: #58a6ff; margin-bottom: 12px; }
91
+ .modal-close { float: right; background: none; border: none; color: #8b949e; cursor: pointer; font-size: 20px; margin-top: -4px; }
92
+
93
+ .empty { padding: 40px; text-align: center; color: #8b949e; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+
98
+ <header>
99
+ <span id="status-dot"></span>
100
+ <h1>๐Ÿค– Copilot Tracer</h1>
101
+ <span class="badge live">LIVE</span>
102
+ <span class="badge" id="session-badge">Session: โ€”</span>
103
+ </header>
104
+
105
+ <div class="stats-bar">
106
+ <div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
107
+ <div class="stat"><span class="stat-label">AI Credits</span><span class="stat-value credits" id="s-credits">0.00 cr | $0.0000</span></div>
108
+ <div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
109
+ <div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
110
+ <div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
111
+ <div class="stat"><span class="stat-label">Skill Calls</span><span class="stat-value skills" id="s-skills">0</span></div>
112
+ <div class="stat"><span class="stat-label">Agent Calls</span><span class="stat-value tokens" id="s-agents">0</span></div>
113
+ <div class="stat"><span class="stat-label">MCP Calls</span><span class="stat-value mcps" id="s-mcps">0</span></div>
114
+ <div class="stat"><span class="stat-label">Total Duration</span><span class="stat-value" id="s-duration">0s</span></div>
115
+ </div>
116
+
117
+ <div class="main">
118
+ <div class="table-panel">
119
+ <table>
120
+ <thead>
121
+ <tr>
122
+ <th>Date / Time</th>
123
+ <th>Prompt</th>
124
+ <th>AI Credits</th>
125
+ <th>Duration</th>
126
+ <th>Cached Tokens</th>
127
+ <th>Written Tokens</th>
128
+ <th>Reasoning Tokens</th>
129
+ <th>Skills</th>
130
+ <th>Agents</th>
131
+ <th>MCPs</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody id="trace-tbody">
135
+ <tr><td colspan="10" class="empty">Waiting for Copilot CLI activity...</td></tr>
136
+ </tbody>
137
+ </table>
138
+ </div>
139
+
140
+ <div class="detail-panel" id="detail-panel">
141
+ <div class="detail-header">
142
+ <h3 id="detail-title">Trace Detail</h3>
143
+ <button class="close-btn" onclick="closeDetail()">ร—</button>
144
+ </div>
145
+ <div class="detail-body" id="detail-body"></div>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- Token detail modal -->
150
+ <div class="modal-overlay" id="token-modal">
151
+ <div class="modal">
152
+ <button class="modal-close" onclick="closeModal()">ร—</button>
153
+ <h3 id="modal-title">Reasoning Detail</h3>
154
+ <div id="modal-body"></div>
155
+ </div>
156
+ </div>
157
+
158
+ <script>
159
+ const socket = io();
160
+ let traces = {};
161
+ let summary = null;
162
+ let selectedId = null;
163
+
164
+ socket.on('init', (data) => {
165
+ data.traces.forEach(t => traces[t.id] = t);
166
+ summary = data.summary;
167
+ render();
168
+ });
169
+
170
+ socket.on('trace:update', (t) => {
171
+ traces[t.id] = t;
172
+ updateSummaryFromTraces();
173
+ render();
174
+ if (selectedId === t.id) renderDetail(t);
175
+ });
176
+
177
+ socket.on('trace:done', (t) => {
178
+ traces[t.id] = t;
179
+ updateSummaryFromTraces();
180
+ render();
181
+ });
182
+
183
+ function updateSummaryFromTraces() {
184
+ const arr = Object.values(traces);
185
+ summary = {
186
+ totalEntries: arr.length,
187
+ totalCredits: arr.reduce((s, t) => s + (t.aiCredits || 0), 0),
188
+ totalTokens: {
189
+ total: arr.reduce((s, t) => s + (t.tokens?.total || 0), 0),
190
+ cached: arr.reduce((s, t) => s + (t.tokens?.cached || 0), 0),
191
+ reasoning: arr.reduce((s, t) => s + (t.tokens?.reasoning || 0), 0),
192
+ },
193
+ totalSkillCalls: arr.reduce((s, t) => s + (t.skillCount || 0), 0),
194
+ totalAgentCalls: arr.reduce((s, t) => s + (t.agentCount || 0), 0),
195
+ totalMcpCalls: arr.reduce((s, t) => s + (t.mcpCount || 0), 0),
196
+ totalDurationMs: arr.reduce((s, t) => s + (t.durationMs || 0), 0),
197
+ };
198
+ }
199
+
200
+ function fmtDuration(ms) {
201
+ if (!ms) return 'โ€”';
202
+ return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's';
203
+ }
204
+
205
+ function fmtCredits(c) { return (c||0).toFixed(2) + ' cr | $' + ((c||0)*0.01).toFixed(4); }
206
+
207
+ function fmtDateTime(dt) {
208
+ if (!dt) return 'โ€”';
209
+ const d = new Date(dt);
210
+ return d.toLocaleDateString('en-US',{month:'2-digit',day:'2-digit'}) + ' ' +
211
+ d.toLocaleTimeString('en-US',{hour12:false});
212
+ }
213
+
214
+ function render() {
215
+ // Stats bar
216
+ if (summary) {
217
+ document.getElementById('s-prompts').textContent = summary.totalEntries;
218
+ document.getElementById('s-credits').textContent = fmtCredits(summary.totalCredits);
219
+ document.getElementById('s-tokens').textContent = (summary.totalTokens?.total || 0).toLocaleString();
220
+ document.getElementById('s-cached').textContent = (summary.totalTokens?.cached || 0).toLocaleString();
221
+ document.getElementById('s-reasoning').textContent = (summary.totalTokens?.reasoning || 0).toLocaleString();
222
+ document.getElementById('s-skills').textContent = summary.totalSkillCalls;
223
+ document.getElementById('s-agents').textContent = summary.totalAgentCalls;
224
+ document.getElementById('s-mcps').textContent = summary.totalMcpCalls;
225
+ document.getElementById('s-duration').textContent = fmtDuration(summary.totalDurationMs);
226
+ }
227
+
228
+ const arr = Object.values(traces).sort((a, b) => new Date(b.dateTime) - new Date(a.dateTime));
229
+ const tbody = document.getElementById('trace-tbody');
230
+
231
+ if (arr.length === 0) {
232
+ tbody.innerHTML = '<tr><td colspan="10" class="empty">Waiting for Copilot CLI activity...</td></tr>';
233
+ return;
234
+ }
235
+
236
+ tbody.innerHTML = arr.map(t => `
237
+ <tr class="${t.status} ${selectedId === t.id ? 'selected' : ''}" onclick="selectTrace('${t.id}')">
238
+ <td class="td-datetime">${fmtDateTime(t.dateTime)}</td>
239
+ <td class="td-prompt" title="${(t.prompt||'').replace(/"/g,'&quot;')}">${escHtml(truncate(t.prompt||'', 60))}</td>
240
+ <td class="td-credits" onclick="event.stopPropagation(); showCreditDetail('${t.id}')">${fmtCredits(t.aiCredits)}</td>
241
+ <td class="td-duration">${fmtDuration(t.durationMs)}</td>
242
+ <td class="td-tokens"><span class="tok-cached">${t.tokens?.cached||0}</span></td>
243
+ <td class="td-tokens"><span class="tok-written">${t.tokens?.written||0}</span></td>
244
+ <td class="td-tokens"><span class="tok-reason" onclick="event.stopPropagation(); showReasoningDetail('${t.id}')">${t.tokens?.reasoning||0}</span></td>
245
+ <td class="td-count"><span class="pill pill-skill" onclick="event.stopPropagation(); showCallDetail('${t.id}','skill')">${t.skillCount||0}</span></td>
246
+ <td class="td-count"><span class="pill pill-agent" onclick="event.stopPropagation(); showCallDetail('${t.id}','agent')">${t.agentCount||0}</span></td>
247
+ <td class="td-count"><span class="pill pill-mcp" onclick="event.stopPropagation(); showCallDetail('${t.id}','mcp')">${t.mcpCount||0}</span></td>
248
+ </tr>
249
+ `).join('');
250
+ }
251
+
252
+ function selectTrace(id) {
253
+ selectedId = id;
254
+ const t = traces[id];
255
+ if (!t) return;
256
+ document.getElementById('detail-panel').classList.add('open');
257
+ renderDetail(t);
258
+ render();
259
+ }
260
+
261
+ function closeDetail() {
262
+ selectedId = null;
263
+ document.getElementById('detail-panel').classList.remove('open');
264
+ render();
265
+ }
266
+
267
+ function renderDetail(t) {
268
+ document.getElementById('detail-title').textContent = truncate(t.prompt || 'Trace Detail', 50);
269
+ const body = document.getElementById('detail-body');
270
+ body.innerHTML = `
271
+ <div class="detail-section">
272
+ <h4>Prompt</h4>
273
+ <div class="detail-box prompt">${escHtml(t.prompt||'')}</div>
274
+ </div>
275
+ ${t.reasoning ? `
276
+ <div class="detail-section">
277
+ <h4>Reasoning (${t.tokens?.reasoning||0} tokens)</h4>
278
+ <div class="detail-box reasoning">${escHtml(t.reasoning)}</div>
279
+ </div>` : ''}
280
+ ${t.response ? `
281
+ <div class="detail-section">
282
+ <h4>Response</h4>
283
+ <div class="detail-box response">${escHtml(t.response)}</div>
284
+ </div>` : ''}
285
+ <div class="detail-section">
286
+ <h4>Token Breakdown</h4>
287
+ <div class="detail-box">
288
+ Input: ${(t.tokens?.input||0).toLocaleString()}
289
+ Cached: ${(t.tokens?.cached||0).toLocaleString()}
290
+ Written: ${(t.tokens?.written||0).toLocaleString()}
291
+ Reasoning: ${(t.tokens?.reasoning||0).toLocaleString()}
292
+ Output: ${(t.tokens?.output||0).toLocaleString()}
293
+ Total: ${(t.tokens?.total||0).toLocaleString()}
294
+ AI Credits: ${fmtCredits(t.aiCredits)}
295
+ </div>
296
+ </div>
297
+ ${t.toolCalls?.length ? `
298
+ <div class="detail-section">
299
+ <h4>Call Graph (${t.toolCalls.length} calls)</h4>
300
+ <div class="call-tree">${renderCallTree(t.toolCalls, 0)}</div>
301
+ </div>` : ''}
302
+ ${t.error ? `
303
+ <div class="detail-section">
304
+ <h4>Error</h4>
305
+ <div class="detail-box" style="color:#f85149">${escHtml(t.error)}</div>
306
+ </div>` : ''}
307
+ `;
308
+ }
309
+
310
+ function renderCallTree(calls, depth) {
311
+ if (!calls || !calls.length) return '';
312
+ return calls.map(c => {
313
+ const typeColor = c.type === 'mcp' ? '#56d364' : c.type === 'skill' ? '#d2a8ff' : c.type === 'agent' ? '#79c0ff' : '#8b949e';
314
+ const dur = c.durationMs ? ` ${c.durationMs}ms` : c.startedAt ? ' running...' : '';
315
+ const inputStr = c.input ? JSON.stringify(c.input, null, 2) : '';
316
+ const outputStr = c.output ? JSON.stringify(c.output, null, 2).slice(0, 300) : '';
317
+ const errorStr = c.error || '';
318
+ const hasChildren = c.children && c.children.length > 0;
319
+ const nodeId = 'node-' + c.id;
320
+
321
+ return `
322
+ <div class="call-node">
323
+ <div class="call-node-header" onclick="toggleNode('${nodeId}')">
324
+ <span class="call-expand">${hasChildren ? 'โ–ถ' : 'ยท'}</span>
325
+ <span class="call-name" style="color:${typeColor}">[${c.type.toUpperCase()}]</span>
326
+ <span>${escHtml(c.name)}</span>
327
+ <span class="call-duration">${dur}</span>
328
+ </div>
329
+ <div id="${nodeId}" style="display:none">
330
+ ${inputStr ? `<div class="call-io"><span class="label">IN:</span>${escHtml(inputStr.slice(0,200))}</div>` : ''}
331
+ ${outputStr ? `<div class="call-io"><span class="label">OUT:</span>${escHtml(outputStr)}</div>` : ''}
332
+ ${errorStr ? `<div class="call-io error"><span class="label">ERR:</span>${escHtml(errorStr)}</div>` : ''}
333
+ ${hasChildren ? `<div class="call-children">${renderCallTree(c.children, depth+1)}</div>` : ''}
334
+ </div>
335
+ </div>
336
+ `;
337
+ }).join('');
338
+ }
339
+
340
+ function toggleNode(id) {
341
+ const el = document.getElementById(id);
342
+ if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
343
+ const header = el?.previousElementSibling?.querySelector('.call-expand');
344
+ if (header) header.textContent = el.style.display === 'none' ? 'โ–ถ' : 'โ–ผ';
345
+ }
346
+
347
+ function showReasoningDetail(id) {
348
+ const t = traces[id];
349
+ if (!t) return;
350
+ document.getElementById('modal-title').textContent = `Reasoning Tokens: ${t.tokens?.reasoning||0}`;
351
+ document.getElementById('modal-body').innerHTML = `
352
+ <div style="margin-bottom:12px; color:#8b949e; font-size:12px">
353
+ Prompt: ${escHtml(truncate(t.prompt||'',80))}
354
+ </div>
355
+ ${t.reasoning ? `
356
+ <div class="detail-box reasoning" style="max-height:300px">${escHtml(t.reasoning)}</div>
357
+ ` : '<p style="color:#8b949e">No reasoning captured for this turn.</p>'}
358
+ ${t.toolCalls?.length ? `
359
+ <h4 style="margin-top:14px; margin-bottom:8px; color:#8b949e; font-size:11px; text-transform:uppercase">Call Graph</h4>
360
+ <div class="call-tree">${renderCallTree(t.toolCalls, 0)}</div>
361
+ ` : ''}
362
+ `;
363
+ document.getElementById('token-modal').classList.add('open');
364
+ }
365
+
366
+ function showCreditDetail(id) {
367
+ const t = traces[id];
368
+ if (!t) return;
369
+ document.getElementById('modal-title').textContent = `AI Credits: ${fmtCredits(t.aiCredits)}`;
370
+ document.getElementById('modal-body').innerHTML = `
371
+ <div class="detail-box" style="line-height:2">
372
+ Input tokens: ${(t.tokens?.input||0).toLocaleString()} ร— $0.003/1k = $${((t.tokens?.input||0)/1000*0.003).toFixed(5)}
373
+ Written tokens: ${(t.tokens?.written||0).toLocaleString()} ร— $0.006/1k = $${((t.tokens?.written||0)/1000*0.006).toFixed(5)}
374
+ Reasoning tokens: ${(t.tokens?.reasoning||0).toLocaleString()} ร— $0.009/1k = $${((t.tokens?.reasoning||0)/1000*0.009).toFixed(5)}
375
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
376
+ Total AI Credits: ${fmtCredits(t.aiCredits)}
377
+ Duration: ${fmtDuration(t.durationMs)}
378
+ </div>
379
+ `;
380
+ document.getElementById('token-modal').classList.add('open');
381
+ }
382
+
383
+ function showCallDetail(id, type) {
384
+ const t = traces[id];
385
+ if (!t) return;
386
+ const calls = (t.toolCalls || []).filter(c => c.type === type);
387
+ const label = type.toUpperCase();
388
+ document.getElementById('modal-title').textContent = `${label} Calls (${calls.length})`;
389
+ document.getElementById('modal-body').innerHTML = calls.length
390
+ ? `<div class="call-tree">${renderCallTree(calls, 0)}</div>`
391
+ : `<p style="color:#8b949e">No ${label} calls in this turn.</p>`;
392
+ document.getElementById('token-modal').classList.add('open');
393
+ }
394
+
395
+ function closeModal() { document.getElementById('token-modal').classList.remove('open'); }
396
+
397
+ document.getElementById('token-modal').addEventListener('click', function(e) {
398
+ if (e.target === this) closeModal();
399
+ });
400
+
401
+ function truncate(s, n) { return s.length > n ? s.slice(0,n-1)+'โ€ฆ' : s; }
402
+ function escHtml(s) {
403
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
404
+ }
405
+ </script>
406
+ </body>
407
+ </html>