Complete suite of HTML dashboards for AI tool usage and conversation reporting via webhook APIs. Includes main navigation dashboard, multiple conversation reports (Latam, SBII, Sidekick), and AI tools usage report with comprehensive analytics. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
699 lines
No EOL
26 KiB
HTML
699 lines
No EOL
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Conversation Reporting Tool</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: 'Montserrat', sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background-color: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
font-size: 22px;
|
|
}
|
|
.controls {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 15px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
select {
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ddd;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 16px;
|
|
}
|
|
button {
|
|
padding: 10px 20px;
|
|
background-color: #4285f4;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
}
|
|
button:hover {
|
|
background-color: #3367d6;
|
|
}
|
|
.loading {
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
.loader {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #3498db;
|
|
border-radius: 50%;
|
|
width: 30px;
|
|
height: 30px;
|
|
animation: spin 2s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
.results {
|
|
margin-top: 15px;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
th, td {
|
|
padding: 6px 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
line-height: 1.3;
|
|
}
|
|
th {
|
|
background-color: #f8f9fa;
|
|
font-weight: 600;
|
|
}
|
|
tr:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
.error {
|
|
color: red;
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
font-weight: 500;
|
|
}
|
|
.export-btn {
|
|
margin-top: 15px;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
.export-btn button {
|
|
padding: 6px 12px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 13px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.export-btn button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
.export-btn svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
.time-filter {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
.time-filter button {
|
|
padding: 6px 12px;
|
|
background-color: #f0f0f0;
|
|
color: #333;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
.time-filter button.active {
|
|
background-color: #4285f4;
|
|
color: white;
|
|
border-color: #4285f4;
|
|
}
|
|
.time-filter button:hover:not(.active) {
|
|
background-color: #e0e0e0;
|
|
}
|
|
.chart-container {
|
|
margin-top: 30px;
|
|
margin-bottom: 30px;
|
|
height: 300px;
|
|
width: 100%;
|
|
}
|
|
.panel {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.panel-title {
|
|
margin-top: 0;
|
|
margin-bottom: 15px;
|
|
font-size: 16px;
|
|
color: #333;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Conversation Reporting Tool</h1>
|
|
|
|
<div class="controls">
|
|
<select id="optionSelect">
|
|
<option value="sidekick_conversations">sidekick_conversations</option>
|
|
</select>
|
|
<button id="sendButton">Send Request</button>
|
|
<div id="exportContainer" class="export-btn" style="display: none;">
|
|
<button id="exportCsvButton">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="7 10 12 15 17 10"></polyline>
|
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
</svg>
|
|
Export CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="time-filter" id="timeFilter" style="display: none;">
|
|
<button data-days="1" class="active">Today</button>
|
|
<button data-days="7">Last 7 Days</button>
|
|
<button data-days="30">Last 30 Days</button>
|
|
<button data-days="90">Last 3 Months</button>
|
|
<button data-days="all">All Time</button>
|
|
</div>
|
|
|
|
<div id="loading" class="loading">
|
|
<div class="loader"></div>
|
|
<p>Loading data, please wait...</p>
|
|
</div>
|
|
|
|
<div id="error" class="error"></div>
|
|
|
|
<div id="usage-chart-container" class="chart-container panel" style="display: none;">
|
|
<h3 class="panel-title">Conversation Usage Over Time</h3>
|
|
<canvas id="usageChart"></canvas>
|
|
</div>
|
|
|
|
<div id="results" class="results">
|
|
<!-- Results will be dynamically inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Global variables
|
|
let currentData = [];
|
|
let assistantData = [];
|
|
let usageChart = null;
|
|
|
|
// Map to store assistant IDs to names
|
|
let assistantNames = {};
|
|
|
|
// CSV Export functionality
|
|
document.getElementById('exportCsvButton').addEventListener('click', () => {
|
|
exportToCSV(currentData);
|
|
});
|
|
|
|
function exportToCSV(data) {
|
|
// Define columns for the CSV
|
|
const columns = [
|
|
'User ID', 'Start Time', 'Brand Voice Setting', 'Title',
|
|
'End Time', 'Assistant ID', 'Assistant Name', 'Assistant Key', 'Conversation ID',
|
|
'Vision Images'
|
|
];
|
|
|
|
// Create CSV header row
|
|
let csvContent = columns.join(',') + '\n';
|
|
|
|
// Add data rows
|
|
data.forEach(item => {
|
|
if (!item.data || Object.keys(item.data).length === 0) return;
|
|
|
|
const d = item.data;
|
|
const assistantId = d.Assistant_ID || '';
|
|
const assistantName = assistantNames[assistantId] || '';
|
|
|
|
const row = [
|
|
d.User_ID ? `"${d.User_ID}"` : '',
|
|
d.StartTime ? `"${d.StartTime}"` : '',
|
|
d['Brand Voice Setting'] ? `"${d['Brand Voice Setting'].replace(/"/g, '""')}"` : '',
|
|
d.Title ? `"${d.Title.replace(/"/g, '""')}"` : '',
|
|
d.EndTime ? `"${d.EndTime}"` : '',
|
|
assistantId ? `"${assistantId}"` : '',
|
|
assistantName ? `"${assistantName.replace(/"/g, '""')}"` : '',
|
|
d.Assistant_Key ? `"${d.Assistant_Key}"` : '',
|
|
d.Conversation_ID ? `"${d.Conversation_ID}"` : '',
|
|
d.Vision_Images ? `"${d.Vision_Images}"` : ''
|
|
];
|
|
|
|
csvContent += row.join(',') + '\n';
|
|
});
|
|
|
|
// Create a download link
|
|
const encodedUri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent);
|
|
const link = document.createElement('a');
|
|
link.setAttribute('href', encodedUri);
|
|
link.setAttribute('download', `sidekick-conversations-${new Date().toISOString().split('T')[0]}.csv`);
|
|
document.body.appendChild(link);
|
|
|
|
// Trigger download and remove link
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// Function to fetch assistant data
|
|
async function fetchAssistantData() {
|
|
try {
|
|
const response = await fetch('https://hook.us1.make.celonis.com/y1o8pqxv40a4qhk55rbsdklpyjyt4nvd', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
auth_code: "jh87dkjs9sdj392sw72sj0210h*^&b$#rfg"
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error fetching assistant data! Status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error fetching assistant data:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Build assistant name mapping
|
|
function buildAssistantMapping(data) {
|
|
const mapping = {};
|
|
|
|
data.forEach(item => {
|
|
if (item.data && item.data['Assistant ID'] && item.data['Name']) {
|
|
mapping[item.data['Assistant ID']] = item.data['Name'];
|
|
}
|
|
});
|
|
|
|
return mapping;
|
|
}
|
|
|
|
document.getElementById('sendButton').addEventListener('click', async () => {
|
|
const selectedOption = document.getElementById('optionSelect').value;
|
|
const loadingDiv = document.getElementById('loading');
|
|
const errorDiv = document.getElementById('error');
|
|
const resultsDiv = document.getElementById('results');
|
|
|
|
// Clear previous results
|
|
errorDiv.textContent = '';
|
|
resultsDiv.innerHTML = '';
|
|
loadingDiv.style.display = 'block';
|
|
|
|
try {
|
|
// First, fetch assistant data in parallel with conversation data
|
|
const assistantDataPromise = fetchAssistantData();
|
|
|
|
// Fetch conversation data
|
|
const response = await fetch('https://hook.us1.make.celonis.com/ewnkda13g1b65s1h2on2clo8ql7vdkl0', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
option: selectedOption,
|
|
auth_code: "jh87dkjs9sdj392sw72sj0210h*^&b$#rfg"
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Wait for assistant data and build mapping
|
|
assistantData = await assistantDataPromise;
|
|
assistantNames = buildAssistantMapping(assistantData);
|
|
|
|
// Store the data globally for CSV export
|
|
currentData = data;
|
|
|
|
// Show export button and time filter
|
|
document.getElementById('exportContainer').style.display = 'block';
|
|
document.getElementById('timeFilter').style.display = 'flex';
|
|
|
|
// Process the data for pivot table
|
|
const pivotData = processPivotData(data);
|
|
|
|
// Initialize the usage chart
|
|
initializeTimeFilter(data);
|
|
createUsageChart(data, 1); // Default to showing 1 day (today)
|
|
|
|
// Generate and display the table
|
|
displayPivotTable(pivotData);
|
|
|
|
} catch (error) {
|
|
errorDiv.textContent = `Error: ${error.message}`;
|
|
console.error('Error:', error);
|
|
document.getElementById('exportContainer').style.display = 'none';
|
|
document.getElementById('timeFilter').style.display = 'none';
|
|
document.getElementById('usage-chart-container').style.display = 'none';
|
|
} finally {
|
|
loadingDiv.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
function processPivotData(data) {
|
|
// Group conversations by Assistant_ID
|
|
const assistantGroups = {};
|
|
|
|
data.forEach(item => {
|
|
const assistantId = item.data.Assistant_ID || 'Unknown';
|
|
const userId = item.data.User_ID || 'Unknown';
|
|
|
|
if (!assistantGroups[assistantId]) {
|
|
assistantGroups[assistantId] = {
|
|
users: {},
|
|
totalCount: 0
|
|
};
|
|
}
|
|
|
|
if (!assistantGroups[assistantId].users[userId]) {
|
|
assistantGroups[assistantId].users[userId] = 0;
|
|
}
|
|
|
|
assistantGroups[assistantId].users[userId]++;
|
|
assistantGroups[assistantId].totalCount++;
|
|
});
|
|
|
|
return assistantGroups;
|
|
}
|
|
|
|
// Initialize time filter buttons
|
|
function initializeTimeFilter(data) {
|
|
const timeFilterButtons = document.querySelectorAll('#timeFilter button');
|
|
|
|
timeFilterButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
// Update active button
|
|
timeFilterButtons.forEach(btn => btn.classList.remove('active'));
|
|
button.classList.add('active');
|
|
|
|
// Get the day filter value
|
|
const days = button.getAttribute('data-days');
|
|
|
|
// Update chart
|
|
createUsageChart(data, days === 'all' ? null : parseInt(days));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Create usage chart
|
|
function createUsageChart(data, days) {
|
|
// Show chart container
|
|
document.getElementById('usage-chart-container').style.display = 'block';
|
|
|
|
// Filter data based on days
|
|
const filteredData = filterDataByDays(data, days);
|
|
|
|
// Process data for the chart
|
|
const { labels, datasets } = processDataForChart(filteredData);
|
|
|
|
// Create or update chart
|
|
if (usageChart) {
|
|
usageChart.data.labels = labels;
|
|
usageChart.data.datasets = datasets;
|
|
usageChart.update();
|
|
} else {
|
|
const ctx = document.getElementById('usageChart').getContext('2d');
|
|
usageChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: false
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
precision: 0,
|
|
stepSize: 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Filter data by time range
|
|
function filterDataByDays(data, days) {
|
|
if (!days) return data; // Return all data if no day filter
|
|
|
|
const now = new Date();
|
|
const cutoffDate = new Date(now);
|
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
|
|
return data.filter(item => {
|
|
if (!item.data || !item.data.StartTime) return false;
|
|
const startTime = new Date(item.data.StartTime);
|
|
return startTime >= cutoffDate;
|
|
});
|
|
}
|
|
|
|
// Process data for chart
|
|
function processDataForChart(data) {
|
|
// Get date range
|
|
const groupedByDate = {};
|
|
const groupedByAssistant = {};
|
|
const assistantColors = {};
|
|
const colorPalette = [
|
|
'#4285F4', '#EA4335', '#FBBC05', '#34A853',
|
|
'#673AB7', '#FF9800', '#00BCD4', '#795548',
|
|
'#9C27B0', '#607D8B', '#3F51B5', '#CDDC39'
|
|
];
|
|
|
|
// Group data by date and assistant
|
|
data.forEach(item => {
|
|
if (!item.data || !item.data.StartTime) return;
|
|
|
|
const date = new Date(item.data.StartTime);
|
|
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
const assistantId = item.data.Assistant_ID || 'Unknown';
|
|
|
|
// Track for this date
|
|
if (!groupedByDate[dateStr]) {
|
|
groupedByDate[dateStr] = {};
|
|
}
|
|
if (!groupedByDate[dateStr][assistantId]) {
|
|
groupedByDate[dateStr][assistantId] = 0;
|
|
}
|
|
groupedByDate[dateStr][assistantId]++;
|
|
|
|
// Track assistants
|
|
if (!groupedByAssistant[assistantId]) {
|
|
groupedByAssistant[assistantId] = 0;
|
|
// Assign color
|
|
const colorIndex = Object.keys(groupedByAssistant).length - 1;
|
|
assistantColors[assistantId] = colorPalette[colorIndex % colorPalette.length];
|
|
}
|
|
groupedByAssistant[assistantId]++;
|
|
});
|
|
|
|
// Sort dates
|
|
const sortedDates = Object.keys(groupedByDate).sort();
|
|
|
|
// Create datasets for each assistant
|
|
const datasets = [];
|
|
|
|
for (const assistantId in groupedByAssistant) {
|
|
// Get assistant name for label if available
|
|
const assistantName = assistantNames[assistantId] || assistantId;
|
|
|
|
const dataset = {
|
|
label: assistantName,
|
|
data: [],
|
|
borderColor: assistantColors[assistantId],
|
|
backgroundColor: assistantColors[assistantId] + '20', // Add transparency
|
|
tension: 0.3,
|
|
fill: true
|
|
};
|
|
|
|
// Add data for each date
|
|
for (const date of sortedDates) {
|
|
dataset.data.push(groupedByDate[date][assistantId] || 0);
|
|
}
|
|
|
|
datasets.push(dataset);
|
|
}
|
|
|
|
return {
|
|
labels: sortedDates,
|
|
datasets: datasets
|
|
};
|
|
}
|
|
|
|
function displayPivotTable(pivotData) {
|
|
const resultsDiv = document.getElementById('results');
|
|
|
|
// Create the table
|
|
const table = document.createElement('table');
|
|
const thead = document.createElement('thead');
|
|
const tbody = document.createElement('tbody');
|
|
|
|
// Create header row
|
|
const headerRow = document.createElement('tr');
|
|
const headers = ['Assistant', 'User ID', 'Conversation Count'];
|
|
|
|
headers.forEach(headerText => {
|
|
const header = document.createElement('th');
|
|
header.textContent = headerText;
|
|
headerRow.appendChild(header);
|
|
});
|
|
|
|
thead.appendChild(headerRow);
|
|
table.appendChild(thead);
|
|
|
|
// Create table body
|
|
for (const assistantId in pivotData) {
|
|
const assistantData = pivotData[assistantId];
|
|
let firstRow = true;
|
|
|
|
for (const userId in assistantData.users) {
|
|
const row = document.createElement('tr');
|
|
|
|
// Assistant column (ID and Name, only show in first row of group)
|
|
const assistantCell = document.createElement('td');
|
|
|
|
if (firstRow) {
|
|
// Get assistant name if available
|
|
const assistantName = assistantNames[assistantId] || null;
|
|
|
|
if (assistantName) {
|
|
// If we have a name, show "Name (ID)"
|
|
assistantCell.textContent = `${assistantName} (${assistantId})`;
|
|
|
|
// Add title attribute for hover tooltip with full ID
|
|
assistantCell.title = assistantId;
|
|
} else {
|
|
// If no name, just show ID
|
|
assistantCell.textContent = assistantId;
|
|
}
|
|
|
|
assistantCell.style.fontWeight = 'bold';
|
|
} else {
|
|
assistantCell.textContent = '';
|
|
}
|
|
|
|
row.appendChild(assistantCell);
|
|
|
|
// User ID column
|
|
const userCell = document.createElement('td');
|
|
userCell.textContent = userId;
|
|
row.appendChild(userCell);
|
|
|
|
// Count column
|
|
const countCell = document.createElement('td');
|
|
countCell.textContent = assistantData.users[userId];
|
|
row.appendChild(countCell);
|
|
|
|
tbody.appendChild(row);
|
|
firstRow = false;
|
|
}
|
|
|
|
// Add a total row for this assistant
|
|
const totalRow = document.createElement('tr');
|
|
|
|
const totalLabelCell = document.createElement('td');
|
|
totalLabelCell.textContent = '';
|
|
totalRow.appendChild(totalLabelCell);
|
|
|
|
const totalUserCell = document.createElement('td');
|
|
totalUserCell.textContent = 'Total';
|
|
totalUserCell.style.fontWeight = 'bold';
|
|
totalRow.appendChild(totalUserCell);
|
|
|
|
const totalCountCell = document.createElement('td');
|
|
totalCountCell.textContent = assistantData.totalCount;
|
|
totalCountCell.style.fontWeight = 'bold';
|
|
totalRow.appendChild(totalCountCell);
|
|
|
|
tbody.appendChild(totalRow);
|
|
|
|
// Add an empty row as a separator
|
|
const spacerRow = document.createElement('tr');
|
|
const spacerCell = document.createElement('td');
|
|
spacerCell.setAttribute('colspan', '3');
|
|
spacerCell.style.height = '4px';
|
|
spacerRow.appendChild(spacerCell);
|
|
tbody.appendChild(spacerRow);
|
|
}
|
|
|
|
// Add grand total row
|
|
const grandTotalRow = document.createElement('tr');
|
|
|
|
// Create grand total cells
|
|
const grandTotalLabelCell = document.createElement('td');
|
|
grandTotalLabelCell.textContent = 'GRAND TOTAL';
|
|
grandTotalLabelCell.style.fontWeight = 'bold';
|
|
grandTotalLabelCell.style.fontSize = '16px';
|
|
grandTotalRow.appendChild(grandTotalLabelCell);
|
|
|
|
const grandTotalBlankCell = document.createElement('td');
|
|
grandTotalRow.appendChild(grandTotalBlankCell);
|
|
|
|
// Calculate grand total
|
|
let grandTotal = 0;
|
|
for (const assistantId in pivotData) {
|
|
grandTotal += pivotData[assistantId].totalCount;
|
|
}
|
|
|
|
const grandTotalCountCell = document.createElement('td');
|
|
grandTotalCountCell.textContent = grandTotal;
|
|
grandTotalCountCell.style.fontWeight = 'bold';
|
|
grandTotalCountCell.style.fontSize = '16px';
|
|
grandTotalRow.appendChild(grandTotalCountCell);
|
|
|
|
tbody.appendChild(grandTotalRow);
|
|
|
|
table.appendChild(tbody);
|
|
resultsDiv.appendChild(table);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |