447 lines
20 KiB
HTML
447 lines
20 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>OPRO Prompt Optimizer</title>
|
|||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|||
|
|
<style>
|
|||
|
|
body { margin: 0; overflow: hidden; }
|
|||
|
|
.chat-container { height: 100vh; display: flex; }
|
|||
|
|
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
|||
|
|
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="chat-container">
|
|||
|
|
<div class="w-64 bg-white border-r border-gray-200 flex flex-col">
|
|||
|
|
<div class="p-4 border-b border-gray-200">
|
|||
|
|
<button onclick="createNewSession()" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|||
|
|
<span>+</span> 新建会话
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="sessionList" class="flex-1 overflow-y-auto scrollbar-hide p-2"></div>
|
|||
|
|
<div class="p-4 border-t border-gray-200">
|
|||
|
|
<div class="text-sm font-semibold text-gray-700 mb-1">选择模型</div>
|
|||
|
|
<div class="flex gap-2">
|
|||
|
|
<select id="modelSelector" class="flex-1 px-2 py-1 border border-gray-300 rounded"></select>
|
|||
|
|
<button onclick="applyModelToSession()" class="px-3 py-1 bg-indigo-500 text-white rounded hover:bg-indigo-600 text-sm">应用</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex-1 flex flex-col bg-white">
|
|||
|
|
<div class="p-4 border-b border-gray-200 bg-gradient-to-r from-blue-500 to-purple-500">
|
|||
|
|
<h1 class="text-xl font-bold text-white">OPRO Prompt Optimizer</h1>
|
|||
|
|
</div>
|
|||
|
|
<div id="chatArea" class="flex-1 overflow-y-auto scrollbar-hide p-4 space-y-4"></div>
|
|||
|
|
<div class="p-4 border-t border-gray-200">
|
|||
|
|
<div class="flex gap-2">
|
|||
|
|
<input type="text" id="messageInput" placeholder="输入消息..."
|
|||
|
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
onkeypress="handleKeyPress(event)">
|
|||
|
|
<button onclick="sendMessage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|||
|
|
发送
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="w-80 bg-white border-l border-gray-200 flex flex-col">
|
|||
|
|
<div class="p-4 border-b border-gray-200 bg-purple-50">
|
|||
|
|
<h2 class="font-bold text-lg">会话信息</h2>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex-1 overflow-y-auto scrollbar-hide p-4">
|
|||
|
|
<div class="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<div class="text-sm font-semibold text-gray-700 mb-1">当前轮次</div>
|
|||
|
|
<div id="roundInfo" class="text-2xl font-bold text-purple-600">Round 0</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="text-sm font-semibold text-gray-700 mb-1">已选提示词</div>
|
|||
|
|
<div id="selectedPromptInfo" class="text-sm text-gray-500 italic">暂未选择</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="text-sm font-semibold text-gray-700 mb-2">操作提示</div>
|
|||
|
|
<div class="text-xs text-gray-600 space-y-1">
|
|||
|
|
<div>• 输入问题后会生成候选提示词</div>
|
|||
|
|
<div>• 点击"选择"使用该提示词</div>
|
|||
|
|
<div>• 点击"拒绝"生成新候选</div>
|
|||
|
|
<div>• 点击"继续优化"获取更多选项</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div id="optimizeBtn" class="p-4 border-t border-gray-200" style="display:none;">
|
|||
|
|
<button onclick="continueOptimization()" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
|
|||
|
|
🔄 继续优化
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="loadingMask" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50" style="display:none;">
|
|||
|
|
<div class="bg-white rounded-lg p-6 shadow-xl">
|
|||
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|||
|
|
<div class="mt-4 text-gray-700">处理中...</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let currentSession = null;
|
|||
|
|
let currentRound = 0;
|
|||
|
|
let candidates = [];
|
|||
|
|
let selectedPrompt = null;
|
|||
|
|
let modelList = [];
|
|||
|
|
let currentModel = '';
|
|||
|
|
const API_BASE = '';
|
|||
|
|
|
|||
|
|
function handleKeyPress(event) {
|
|||
|
|
if (event.key === 'Enter') {
|
|||
|
|
sendMessage();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showLoading(show) {
|
|||
|
|
document.getElementById('loadingMask').style.display = show ? 'flex' : 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateSessionInfo() {
|
|||
|
|
document.getElementById('roundInfo').textContent = 'Round ' + currentRound;
|
|||
|
|
const selectedInfo = document.getElementById('selectedPromptInfo');
|
|||
|
|
if (selectedPrompt) {
|
|||
|
|
selectedInfo.textContent = selectedPrompt.length > 100 ? selectedPrompt.substring(0, 100) + '...' : selectedPrompt;
|
|||
|
|
selectedInfo.className = 'text-sm text-gray-700 bg-green-50 p-2 rounded border border-green-200';
|
|||
|
|
} else {
|
|||
|
|
selectedInfo.textContent = '暂未选择';
|
|||
|
|
selectedInfo.className = 'text-sm text-gray-500 italic';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadSessions() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/sessions');
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
const sessionList = document.getElementById('sessionList');
|
|||
|
|
sessionList.innerHTML = '';
|
|||
|
|
|
|||
|
|
const sessions = payload.sessions || [];
|
|||
|
|
for (let i = 0; i < sessions.length; i++) {
|
|||
|
|
const session = sessions[i];
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
const isActive = currentSession === session.session_id;
|
|||
|
|
div.className = 'p-3 mb-2 rounded-lg cursor-pointer transition-colors ' +
|
|||
|
|
(isActive ? 'bg-blue-50 border-2 border-blue-300' : 'bg-gray-50 hover:bg-gray-100 border-2 border-transparent');
|
|||
|
|
div.onclick = function() { loadSession(session.session_id); };
|
|||
|
|
div.innerHTML = '<div class="font-medium text-sm truncate">' +
|
|||
|
|
(session.original_query || '未命名会话') + '</div>' +
|
|||
|
|
'<div class="text-xs text-gray-500 mt-1">Round ' + session.round + '</div>';
|
|||
|
|
sessionList.appendChild(div);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('加载会话失败:', err);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadModels() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/models');
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
modelList = payload.models || [];
|
|||
|
|
const sel = document.getElementById('modelSelector');
|
|||
|
|
if (sel) {
|
|||
|
|
sel.innerHTML = '';
|
|||
|
|
for (let i = 0; i < modelList.length; i++) {
|
|||
|
|
const opt = document.createElement('option');
|
|||
|
|
opt.value = modelList[i];
|
|||
|
|
opt.textContent = modelList[i];
|
|||
|
|
sel.appendChild(opt);
|
|||
|
|
}
|
|||
|
|
if (!currentModel && modelList.length > 0) {
|
|||
|
|
currentModel = modelList[0];
|
|||
|
|
sel.value = currentModel;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (err) { /* ignore */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function applyModelToSession() {
|
|||
|
|
try {
|
|||
|
|
const sel = document.getElementById('modelSelector');
|
|||
|
|
currentModel = sel ? sel.value : currentModel;
|
|||
|
|
if (!currentSession || !currentModel) return;
|
|||
|
|
await fetch(API_BASE + '/set_model', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ session_id: currentSession, model_name: currentModel })
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('应用模型失败: ' + err.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function createNewSession() {
|
|||
|
|
currentSession = null;
|
|||
|
|
currentRound = 0;
|
|||
|
|
candidates = [];
|
|||
|
|
selectedPrompt = null;
|
|||
|
|
document.getElementById('chatArea').innerHTML = '';
|
|||
|
|
document.getElementById('optimizeBtn').style.display = 'none';
|
|||
|
|
updateSessionInfo();
|
|||
|
|
document.getElementById('messageInput').focus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadSession(sessionId) {
|
|||
|
|
showLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/session/' + sessionId);
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
|
|||
|
|
currentSession = sessionId;
|
|||
|
|
currentRound = payload.round;
|
|||
|
|
candidates = payload.candidates || [];
|
|||
|
|
selectedPrompt = payload.selected_prompt;
|
|||
|
|
|
|||
|
|
document.getElementById('chatArea').innerHTML = '';
|
|||
|
|
|
|||
|
|
appendMessage('user', payload.original_query);
|
|||
|
|
|
|||
|
|
if (payload.candidates && payload.candidates.length > 0) {
|
|||
|
|
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (payload.history) {
|
|||
|
|
for (let i = 0; i < payload.history.length; i++) {
|
|||
|
|
const msg = payload.history[i];
|
|||
|
|
appendMessage(msg.role, msg.content);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateSessionInfo();
|
|||
|
|
document.getElementById('optimizeBtn').style.display = candidates.length > 0 ? 'block' : 'none';
|
|||
|
|
await loadSessions();
|
|||
|
|
await loadModels();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('加载会话失败: ' + err.message);
|
|||
|
|
} finally {
|
|||
|
|
showLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function sendMessage() {
|
|||
|
|
const input = document.getElementById('messageInput');
|
|||
|
|
const msg = input.value.trim();
|
|||
|
|
if (!msg) return;
|
|||
|
|
|
|||
|
|
input.value = '';
|
|||
|
|
showLoading(true);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (!currentSession) {
|
|||
|
|
const res = await fetch(API_BASE + '/query', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ query: msg })
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
|
|||
|
|
currentSession = payload.session_id;
|
|||
|
|
currentRound = payload.round;
|
|||
|
|
candidates = payload.candidates || [];
|
|||
|
|
|
|||
|
|
if (currentModel) {
|
|||
|
|
await fetch(API_BASE + '/set_model', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ session_id: currentSession, model_name: currentModel })
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
appendMessage('user', msg);
|
|||
|
|
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
|
|||
|
|
updateSessionInfo();
|
|||
|
|
document.getElementById('optimizeBtn').style.display = 'block';
|
|||
|
|
await loadSessions();
|
|||
|
|
} else {
|
|||
|
|
appendMessage('user', msg);
|
|||
|
|
|
|||
|
|
const candRes = await fetch(API_BASE + '/query_from_message', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ session_id: currentSession })
|
|||
|
|
});
|
|||
|
|
const candData = await candRes.json();
|
|||
|
|
const payload = candData && candData.data ? candData.data : {};
|
|||
|
|
|
|||
|
|
candidates = payload.candidates || [];
|
|||
|
|
currentRound = payload.round;
|
|||
|
|
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
|
|||
|
|
updateSessionInfo();
|
|||
|
|
document.getElementById('optimizeBtn').style.display = 'block';
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('发送失败: ' + err.message);
|
|||
|
|
} finally {
|
|||
|
|
showLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function appendMessage(role, content) {
|
|||
|
|
const chatArea = document.getElementById('chatArea');
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
|
|||
|
|
if (role === 'user') {
|
|||
|
|
div.className = 'flex justify-end';
|
|||
|
|
div.innerHTML = '<div class="max-w-2xl bg-blue-500 text-white rounded-lg px-4 py-3">' +
|
|||
|
|
'<div class="font-semibold mb-1 text-sm">👤 用户</div>' +
|
|||
|
|
'<div>' + escapeHtml(content) + '</div>' +
|
|||
|
|
'</div>';
|
|||
|
|
} else if (role === 'assistant') {
|
|||
|
|
div.className = 'flex justify-start';
|
|||
|
|
div.innerHTML = '<div class="max-w-3xl bg-green-50 rounded-lg px-4 py-3 border border-green-200">' +
|
|||
|
|
'<div class="font-semibold mb-2 text-sm text-green-700">🤖 AI 回答</div>' +
|
|||
|
|
'<div class="whitespace-pre-wrap">' + escapeHtml(content) + '</div>' +
|
|||
|
|
'</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
chatArea.appendChild(div);
|
|||
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function appendCandidateMessage(content, candidateList) {
|
|||
|
|
const chatArea = document.getElementById('chatArea');
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
div.className = 'flex justify-start';
|
|||
|
|
|
|||
|
|
let candidatesHtml = '';
|
|||
|
|
if (candidateList && candidateList.length > 0) {
|
|||
|
|
candidatesHtml = '<div class="space-y-3 mt-3">';
|
|||
|
|
for (let i = 0; i < candidateList.length; i++) {
|
|||
|
|
const cand = candidateList[i];
|
|||
|
|
candidatesHtml += '<div class="bg-white border-2 border-gray-200 rounded-lg p-3 hover:border-blue-300 transition-colors">' +
|
|||
|
|
'<div class="flex items-start gap-3">' +
|
|||
|
|
'<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center text-purple-600 font-bold text-sm">' + (i + 1) + '</div>' +
|
|||
|
|
'<div class="flex-1">' +
|
|||
|
|
'<div class="text-sm text-gray-700 mb-3">' + escapeHtml(cand) + '</div>' +
|
|||
|
|
'<div class="flex gap-2">' +
|
|||
|
|
'<button onclick="selectCandidate(' + i + ')" class="flex-1 px-3 py-1.5 bg-green-500 text-white rounded hover:bg-green-600 text-sm font-medium transition-colors">✓ 选择</button>' +
|
|||
|
|
'<button onclick="rejectCandidate(' + i + ')" class="flex-1 px-3 py-1.5 bg-red-500 text-white rounded hover:bg-red-600 text-sm font-medium transition-colors">✗ 拒绝</button>' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>';
|
|||
|
|
}
|
|||
|
|
candidatesHtml += '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
div.innerHTML = '<div class="max-w-4xl bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg px-4 py-3 border-2 border-purple-200">' +
|
|||
|
|
'<div class="font-semibold mb-2 text-purple-700">🤖 系统推荐</div>' +
|
|||
|
|
'<div class="mb-2 text-sm text-gray-600">' + escapeHtml(content) + '</div>' +
|
|||
|
|
candidatesHtml +
|
|||
|
|
'</div>';
|
|||
|
|
|
|||
|
|
chatArea.appendChild(div);
|
|||
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function selectCandidate(idx) {
|
|||
|
|
if (!currentSession) return;
|
|||
|
|
const candidate = candidates[idx];
|
|||
|
|
|
|||
|
|
showLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/select', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ session_id: currentSession, choice: candidate })
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
|
|||
|
|
selectedPrompt = candidate;
|
|||
|
|
appendMessage('user', '✅ 确认选择 Prompt ' + (idx + 1));
|
|||
|
|
appendMessage('assistant', payload.answer);
|
|||
|
|
updateSessionInfo();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('选择失败: ' + err.message);
|
|||
|
|
} finally {
|
|||
|
|
showLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function rejectCandidate(idx) {
|
|||
|
|
if (!currentSession) return;
|
|||
|
|
const candidate = candidates[idx];
|
|||
|
|
const reason = prompt('请说明拒绝理由(可选):');
|
|||
|
|
|
|||
|
|
showLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/reject', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
session_id: currentSession,
|
|||
|
|
candidate: candidate,
|
|||
|
|
reason: reason
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
|
|||
|
|
candidates = payload.candidates || [];
|
|||
|
|
currentRound = payload.round;
|
|||
|
|
|
|||
|
|
appendMessage('user', '❌ 拒绝 Prompt ' + (idx + 1) + (reason ? ':' + reason : ''));
|
|||
|
|
appendCandidateMessage('已生成新的候选提示词:', payload.candidates);
|
|||
|
|
updateSessionInfo();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('拒绝失败: ' + err.message);
|
|||
|
|
} finally {
|
|||
|
|
showLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function continueOptimization() {
|
|||
|
|
if (!currentSession) return;
|
|||
|
|
|
|||
|
|
showLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API_BASE + '/query_from_message', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ session_id: currentSession })
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
const payload = data && data.data ? data.data : {};
|
|||
|
|
|
|||
|
|
candidates = payload.candidates || [];
|
|||
|
|
currentRound = payload.round;
|
|||
|
|
|
|||
|
|
appendMessage('user', '🔄 继续优化提示词');
|
|||
|
|
appendCandidateMessage('新一轮候选提示词:', payload.candidates);
|
|||
|
|
updateSessionInfo();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('优化失败: ' + err.message);
|
|||
|
|
} finally {
|
|||
|
|
showLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function escapeHtml(text) {
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
div.textContent = text;
|
|||
|
|
return div.innerHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.onload = function() {
|
|||
|
|
loadSessions();
|
|||
|
|
loadModels();
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|