Files
turf_saas/agent_ia.html
2026-04-25 17:18:43 +02:00

610 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent IA</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f23; color: #eee; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
header { background: linear-gradient(90deg, #1a1a2e, #0f3460); padding: 10px 20px; border-bottom: 2px solid #00d9ff; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
header h1 { font-size: 18px; background: linear-gradient(90deg, #00d9ff, #e94560); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
header .config-link { color: #888; font-size: 12px; text-decoration: none; }
header .config-link:hover { color: #00d9ff; }
.tabs { display: flex; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
.tab { padding: 10px 20px; cursor: pointer; border: none; background: none; color: #888; font-size: 14px; border-bottom: 2px solid transparent; transition: all 0.2s; }
.tab:hover { color: #eee; background: rgba(0,217,255,0.05); }
.tab.active { color: #00d9ff; border-bottom-color: #00d9ff; }
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-header { padding: 12px; border-bottom: 1px solid #30363d; }
.new-chat-btn { width: 100%; padding: 8px; background: #00d9ff; color: #0f0f23; border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.new-chat-btn:hover { background: #00b8d9; }
.session-list { flex: 1; overflow-y: auto; padding: 8px; }
.session-item { padding: 10px 12px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; transition: background 0.15s; }
.session-item:hover { background: rgba(0,217,255,0.08); }
.session-item.active { background: rgba(0,217,255,0.15); border-left: 3px solid #00d9ff; }
.session-title { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
.session-date { font-size: 10px; color: #555; margin-top: 2px; }
.session-delete { opacity: 0; background: none; border: none; color: #d23232; cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 4px; }
.session-item:hover .session-delete { opacity: 1; }
.session-delete:hover { background: rgba(210,50,50,0.2); }
.session-rename { opacity: 0; background: none; border: none; color: #888; cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; margin-right: 4px; }
.session-item:hover .session-rename { opacity: 1; }
.session-rename:hover { color: #00d9ff; background: rgba(0,217,255,0.1); }
.session-edit-input { background: #0d1117; border: 1px solid #00d9ff; border-radius: 4px; color: #eee; font-size: 12px; padding: 4px 8px; width: 100%; outline: none; }
.msg-copy { opacity: 0; background: none; border: none; color: #888; cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 4px; margin-top: 8px; display: inline-flex; align-items: center; gap: 4px; }
.msg.ai:hover .msg-copy { opacity: 1; }
.msg-copy:hover { color: #00d9ff; background: rgba(0,217,255,0.1); }
.msg-copied { color: #4caf50; font-size: 12px; margin-left: 8px; }
.chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
#chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
.welcome { text-align: center; color: #555; margin-top: 20vh; }
.welcome h2 { color: #888; margin-bottom: 8px; }
.msg { max-width: 75%; padding: 12px 16px; border-radius: 12px; line-height: 1.6; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; }
.msg.user { align-self: flex-end; background: #0f3460; border: 1px solid #00d9ff; border-bottom-right-radius: 4px; }
.msg.ai { align-self: flex-start; background: #161b22; border: 1px solid #30363d; border-bottom-left-radius: 4px; }
.msg.ai p { margin: 0 0 8px 0; }
.msg.ai p:last-child { margin-bottom: 0; }
.msg.ai code { background: #0d1117; padding: 2px 6px; border-radius: 4px; font-size: 13px; font-family: 'Fira Code', monospace; }
.msg.ai pre { background: #0d1117; padding: 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; }
.msg.ai pre code { background: none; padding: 0; }
.msg.ai ul, .msg.ai ol { padding-left: 20px; margin: 8px 0; }
.msg.ai li { margin-bottom: 4px; }
.msg.ai table { border-collapse: collapse; margin: 8px 0; }
.msg.ai th, .msg.ai td { border: 1px solid #30363d; padding: 6px 10px; font-size: 13px; }
.msg.ai th { background: #1a1a2e; }
.msg.error { align-self: center; background: rgba(210,50,50,0.2); border: 1px solid #d23232; color: #f88; }
.typing { align-self: flex-start; color: #666; font-style: italic; font-size: 13px; padding: 8px 16px; display: flex; align-items: center; gap: 8px; }
.typing-dots { display: flex; gap: 4px; }
.typing-dot { width: 6px; height: 6px; background: #666; border-radius: 50%; animation: typingBounce 1.4s infinite ease-in-out; }
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } }
.search-bar { padding: 8px 12px; border-bottom: 1px solid #30363d; }
.search-input { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #eee; padding: 8px 10px; font-size: 12px; outline: none; }
.search-input:focus { border-color: #00d9ff; }
.search-results { max-height: 200px; overflow-y: auto; padding: 8px; }
.search-result { padding: 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; font-size: 12px; }
.search-result:hover { background: rgba(0,217,255,0.08); }
.search-result-mark { background: rgba(255,215,0,0.2); padding: 2px 4px; border-radius: 3px; }
.export-btn { background: none; border: none; color: #888; cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 4px; }
.export-btn:hover { color: #00d9ff; background: rgba(0,217,255,0.1); }
.session-actions { display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid #30363d; }
#input-area { display: flex; gap: 10px; padding: 15px 20px; background: #161b22; border-top: 1px solid #30363d; flex-shrink: 0; }
#input-area textarea { flex: 1; background: #0d1117; border: 1px solid #30363d; border-radius: 8px; color: #eee; padding: 12px; font-size: 14px; resize: none; font-family: inherit; min-height: 48px; max-height: 120px; outline: none; }
#input-area textarea:focus { border-color: #00d9ff; }
#model-select { padding: 8px 12px; border-radius: 8px; border: 1px solid #30363d; background: #161b22; color: #e6edf3; font-size: 14px; cursor: pointer; }
#model-select:hover { border-color: #00d9ff; }
#send-btn { background: #00d9ff; color: #0f0f23; border: none; border-radius: 8px; padding: 0 20px; font-size: 16px; font-weight: bold; cursor: pointer; transition: background 0.2s; }
#send-btn:hover { background: #00b8d9; }
#send-btn:disabled { background: #30363d; color: #666; cursor: not-allowed; }
@media (max-width: 768px) {
.sidebar { display: none; }
.sidebar.open { display: flex; position: fixed; top: 0; left: 0; bottom: 0; z-index: 100; width: 280px; }
.msg { max-width: 90%; }
#input-area { padding: 10px; }
.mobile-menu-btn { display: block !important; }
}
.mobile-menu-btn { display: none; background: none; border: none; color: #eee; font-size: 20px; cursor: pointer; padding: 4px 8px; }
.overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 99; }
.overlay.active { display: block; }
.home-btn{position:fixed;top:10px;left:10px;z-index:9999;background:linear-gradient(135deg,#00d9ff,#7b2cbf);color:#fff;border:none;border-radius:8px;padding:10px 15px;font-size:14px;cursor:pointer;text-decoration:none;display:flex;align-items:center;gap:8px;box-shadow:0 2px 10px rgba(0,217,255,0.3);transition:all 0.3s;font-family:-apple-system,BlinkMacSystemFont,sans-serif}.home-btn:hover{transform:translateY(-2px);box-shadow:0 4px 15px rgba(0,217,255,0.5)}
</style>
</head>
<body>
<a href="https://portal-kolifee.duckdns.org/" class="home-btn" title="Retour au portail"><span style="font-size:18px">🏠</span><span>Accueil</span></a>
<header>
<div style="display:flex;align-items:center;gap:8px;">
<button class="mobile-menu-btn" onclick="toggleSidebar()">&#9776;</button>
<h1>Agent IA</h1>
</div>
<a href="/agent-ia/config" class="config-link">&#9881; Config</a>
<button class="export-btn" onclick="showExportMenu()" title="Exporter">&#128190; Export</button>
</header>
<div class="tabs" id="tabs"></div>
<div class="overlay" id="overlay" onclick="toggleSidebar()"></div>
<div class="main">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="newConversation()">+ Nouvelle conversation</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="search-input" placeholder="Rechercher..." oninput="searchMessages(this.value)">
</div>
<div class="search-results" id="search-results" style="display:none;"></div>
<div class="session-list" id="session-list"></div>
</div>
<div class="chat-area">
<div id="chat">
<div class="welcome">
<h2>Bienvenue sur Agent IA</h2>
<p>Sélectionnez un onglet et commencez une conversation</p>
</div>
</div>
<div id="input-area">
<select id="model-select" style="display: none;"><option value="llama-3.1-8b">Llama 3.1 8B</option></select>
<textarea id="user-input" placeholder="Tapez votre message..." rows="1"></textarea>
<button id="send-btn">Envoyer</button>
</div>
</div>
</div>
<script>
const chatEl = document.getElementById('chat');
const input = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const tabsEl = document.getElementById('tabs');
const sessionListEl = document.getElementById('session-list');
let workflows = [];
let nvidiaModels = {};
let selectedModel = "llama-3.1-8b";
let activeWorkflow = null;
let activeSession = null;
let sessionId = generateSessionId();
function generateSessionId() {
return 'session-' + Math.random().toString(36).substr(2, 9);
}
async function loadWorkflows() {
try {
const resp = await fetch('/api/chat/workflows');
workflows = await resp.json();
if (workflows.length === 0) return;
// Show model select if nvidia-chat is available
const nvidiaWorkflow = workflows.find(w => w.slug === "nvidia-chat");
const modelSelect = document.getElementById("model-select");
if (nvidiaWorkflow) {
modelSelect.style.display = "block";
loadNvidiaModels();
} else {
modelSelect.style.display = "none";
}
renderTabs();
selectWorkflow(workflows[0].slug);
} catch (e) {
console.error('Erreur chargement workflows:', e);
}
}
async function loadNvidiaModels() {
try {
const resp = await fetch("/api/chat/nvidia-models");
nvidiaModels = await resp.json();
const select = document.getElementById("model-select");
select.innerHTML = "";
nvidiaModels.forEach(m => {
const opt = document.createElement("option");
opt.value = m.id;
opt.textContent = m.name;
select.appendChild(opt);
});
} catch (e) {
console.error("Error loading models:", e);
}
}
function renderTabs() {
tabsEl.innerHTML = workflows.map(wf =>
`<button class="tab ${wf.slug === activeWorkflow ? 'active' : ''}" data-slug="${wf.slug}" onclick="selectWorkflow('${wf.slug}')">${wf.name}</button>`
).join('');
}
async function selectWorkflow(slug) {
activeWorkflow = slug;
activeSession = null;
sessionId = generateSessionId();
renderTabs();
renderSessions();
clearChat();
showWelcome();
}
async function renderSessions() {
if (!activeWorkflow) return;
try {
const resp = await fetch(`/api/chat/sessions?workflow=${activeWorkflow}`);
const sessions = await resp.json();
sessionListEl.innerHTML = sessions.map(s => {
const date = new Date(s.updated_at).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
const title = s.title || 'Nouvelle conversation';
const isActive = s.session_id === activeSession;
const sid = s.session_id;
const safeTitle = escHtml(title);
const safeId = escHtml(sid);
return `<div class="session-item ${isActive ? 'active' : ''}" onclick="loadSession('${safeId}')" data-sid="${safeId}">
<div style="flex:1;min-width:0;">
<div class="session-title">${safeTitle}</div>
<div class="session-date">${date}</div>
</div>
<button class="session-rename" onclick="event.stopPropagation();startRename('${safeId}', &quot;${safeTitle}&quot;)" title="Renommer">&#9998;</button>
<button class="session-delete" onclick="event.stopPropagation();deleteSession('${safeId}')" title="Supprimer">&#10005;</button>
</div>`;
}).join('');
} catch (e) {
console.error('Erreur sessions:', e);
}
}
async function loadSession(sid) {
activeSession = sid;
sessionId = sid;
renderSessions();
clearChat();
try {
const resp = await fetch(`/api/chat/history?session_id=${sid}&workflow=${activeWorkflow}`);
const messages = await resp.json();
if (messages.length === 0) {
showWelcome();
return;
}
messages.forEach(m => addMessage(m.content, m.role, false));
scrollToBottom();
} catch (e) {
console.error('Erreur historique:', e);
}
}
async function deleteSession(sid) {
if (!confirm('Supprimer cette conversation ?')) return;
try {
await fetch(`/api/chat/session?session_id=${sid}&workflow=${activeWorkflow}`, { method: 'DELETE' });
if (activeSession === sid) {
activeSession = null;
sessionId = generateSessionId();
clearChat();
showWelcome();
}
renderSessions();
} catch (e) {
console.error('Erreur suppression:', e);
}
}
function newConversation() {
activeSession = null;
sessionId = generateSessionId();
renderSessions();
clearChat();
showWelcome();
input.focus();
}
function clearChat() {
chatEl.innerHTML = '';
}
function showWelcome() {
const wf = workflows.find(w => w.slug === activeWorkflow);
const name = wf ? wf.name : 'Agent IA';
chatEl.innerHTML = `<div class="welcome"><h2>${escHtml(name)}</h2><p>Démarrez une nouvelle conversation</p></div>`;
}
let renamingSession = null;
function addMessage(text, type, scroll = true) {
const existing = chatEl.querySelector('.welcome');
if (existing) existing.remove();
const div = document.createElement('div');
div.className = 'msg ' + type;
if (type === 'ai') {
div.innerHTML = marked.parse(text);
const copyBtn = document.createElement('button');
copyBtn.className = 'msg-copy';
copyBtn.innerHTML = '📋 Copier';
copyBtn.onclick = () => copyToClipboard(text, copyBtn);
div.appendChild(copyBtn);
const excelBtn = document.createElement('button');
excelBtn.className = 'msg-copy';
excelBtn.innerHTML = '📊 Excel';
excelBtn.onclick = () => copyToExcel(text, excelBtn);
div.appendChild(excelBtn);
} else {
div.textContent = text;
}
chatEl.appendChild(div);
if (scroll) scrollToBottom();
}
function addTyping() {
const div = document.createElement('div');
div.className = 'typing';
div.id = 'typing-indicator';
div.innerHTML = '<div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div><span>Réflexion en cours...</span>';
chatEl.appendChild(div);
scrollToBottom();
}
function removeTyping() {
const el = document.getElementById('typing-indicator');
if (el) el.remove();
}
function scrollToBottom() {
chatEl.scrollTop = chatEl.scrollHeight;
}
function escHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getPlainText(html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || '';
}
function copyToClipboard(text, btn) {
const plainText = getPlainText(text);
navigator.clipboard.writeText(plainText).then(() => {
const originalHtml = btn.innerHTML;
btn.innerHTML = '<span class="msg-copied">Copié!</span>';
setTimeout(() => { btn.innerHTML = originalHtml; }, 1500);
}).catch(err => {
console.error('Erreur copy:', err);
});
}
function copyToExcel(text, btn) {
const div = document.createElement('div');
div.innerHTML = text;
let tsv = '';
const tables = div.querySelectorAll('table');
if (tables.length > 0) {
tables.forEach(table => {
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('th, td');
const rowData = Array.from(cells).map(c => c.textContent.trim().replace(/\t/g, ' ').replace(/\n/g, ' '));
tsv += rowData.join('\t') + '\n';
});
});
}
if (tsv) {
navigator.clipboard.writeText(tsv).then(() => {
btn.innerHTML = '<span class="msg-copied">Excel copié!</span>';
setTimeout(() => { btn.innerHTML = '📊 Excel'; }, 1500);
});
}
}
async function renameSession(sid, newTitle) {
if (!newTitle || !newTitle.trim()) {
renderSessions();
return;
}
try {
const resp = await fetch(`/api/chat/session?session_id=${sid}&workflow=${activeWorkflow}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.trim() })
});
if (resp.ok) {
renderSessions();
} else {
const err = await resp.text();
console.error('Erreur rename:', err);
}
} catch (e) {
console.error('Erreur rename:', e);
}
}
function createRenameInput(sid, currentTitle) {
const input = document.createElement('input');
input.type = 'text';
input.className = 'session-edit-input';
input.value = currentTitle || '';
input.placeholder = 'Titre de la conversation';
input.onkeydown = async (e) => {
if (e.key === 'Enter') {
await renameSession(sid, input.value);
renderSessions();
} else if (e.key === 'Escape') {
renderSessions();
}
};
input.onblur = async () => {
await renameSession(sid, input.value);
renderSessions();
};
return input;
}
function startRename(sid, currentTitle) {
renamingSession = sid;
const items = sessionListEl.querySelectorAll('.session-item');
items.forEach(item => {
if (item.dataset.sid === sid) {
const titleDiv = item.querySelector('.session-title');
titleDiv.innerHTML = '';
titleDiv.appendChild(createRenameInput(sid, currentTitle));
const input = titleDiv.querySelector('input');
if (input) input.focus();
}
});
}
async function sendMessage() {
const text = input.value.trim();
if (!text || !activeWorkflow) return;
addMessage(text, 'user');
input.value = '';
input.style.height = 'auto';
sendBtn.disabled = true;
addTyping();
if (!activeSession) {
activeSession = sessionId;
try {
await fetch('/api/chat/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, workflow: activeWorkflow, title: text.substring(0, 50) })
});
} catch (e) {}
renderSessions();
}
try {
const resp = await fetch(`/webhook/${activeWorkflow}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
body: JSON.stringify({
message: text,
model: activeWorkflow === "nvidia-chat" ? selectedModel : undefined
})
});
removeTyping();
if (!resp.ok) {
const err = await resp.text().catch(() => 'Erreur serveur');
addMessage('Erreur: ' + err, 'error');
return;
}
const data = await resp.text();
addMessage(data, 'ai');
} catch (e) {
removeTyping();
addMessage('Erreur de connexion: ' + e.message, 'error');
} finally {
sendBtn.disabled = false;
input.focus();
}
}
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.getElementById('overlay').classList.toggle('active');
}
sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
});
async function searchMessages(query) {
const resultsEl = document.getElementById('search-results');
if (!query || query.length < 2) {
resultsEl.style.display = 'none';
return;
}
resultsEl.style.display = 'block';
resultsEl.innerHTML = '<div style="color:#666;font-size:11px;">Recherche en cours...</div>';
try {
const resp = await fetch(`/api/chat/search?q=${encodeURIComponent(query)}&workflow=${activeWorkflow}`);
const results = await resp.json();
if (results.length === 0) {
resultsEl.innerHTML = '<div style="color:#666;font-size:11px;">Aucun résultat</div>';
return;
}
resultsEl.innerHTML = results.slice(0, 10).map(r => {
const preview = r.content.substring(0, 80).replace(/</g, '&lt;').replace(/>/g, '&gt;');
const markQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const highlighted = preview.replace(new RegExp(markQuery, 'gi'), m => `<span class="search-result-mark">${m}</span>`);
return `<div class="search-result" onclick="loadSession('${r.session_id}');document.getElementById('search-results').style.display='none';">
<div style="color:#888;font-size:10px;">${new Date(r.created_at).toLocaleDateString()}</div>
<div>${highlighted}</div>
</div>`;
}).join('');
} catch (e) {
resultsEl.innerHTML = '<div style="color:#d23232;font-size:11px;">Erreur de recherche</div>';
}
}
async function exportSession(sid, format) {
try {
const resp = await fetch(`/api/chat/history?session_id=${sid}&workflow=${activeWorkflow}`);
const messages = await resp.json();
if (messages.length === 0) {
alert('Aucune conversation à exporter');
return;
}
const session = messages[0].session_id;
let content = '';
let filename = `conversation_${session}_${new Date().toISOString().slice(0,10)}`;
if (format === 'markdown') {
content = `# Conversation\n\n`;
messages.forEach(m => {
content += `## ${m.role === 'user' ? 'Vous' : 'IA'}\n\n${m.content}\n\n---\n\n`;
});
content += `\n*Exporté le ${new Date().toLocaleString()}*`;
downloadFile(content, filename + '.md', 'text/markdown');
} else if (format === 'json') {
content = JSON.stringify({ session_id: session, exported_at: new Date().toISOString(), messages: messages }, null, 2);
downloadFile(content, filename + '.json', 'application/json');
}
} catch (e) {
alert('Erreur export: ' + e.message);
}
}
function downloadFile(content, filename, type) {
const blob = new Blob([content], { type: type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function showExportMenu() {
if (!activeSession) {
alert('Sélectionnez d\'abord une conversation');
return;
}
const choice = prompt('Exporter en:\n1. Markdown\n2. JSON\n\nTapez 1 ou 2:');
if (choice === '1') exportSession(activeSession, 'markdown');
else if (choice === '2') exportSession(activeSession, 'json');
}
// Event listener for model select
document.getElementById("model-select").addEventListener("change", (e) => {
selectedModel = e.target.value;
});
loadWorkflows();
input.focus();
</script>
</body>
</html>