|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<title>GOXY — Чат и модерация</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
:root { |
|
|
--bg: #0b0f14; |
|
|
--card: #111722; |
|
|
--text: #e7edf7; |
|
|
--muted: #9db0c9; |
|
|
--accent: #4f8cff; |
|
|
--ok: #1db954; |
|
|
--bad: #ff4d4f; |
|
|
--user-msg: #1e2a3b; |
|
|
--assistant-msg: #1a2332; |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
margin: 0; |
|
|
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto; |
|
|
background: var(--bg); |
|
|
color: var(--text); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
height: 100vh; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
padding: 0 16px; |
|
|
width: 100%; |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.header { |
|
|
padding: 20px 0; |
|
|
border-bottom: 1px solid #1b2331; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 20px; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.main-content { |
|
|
display: flex; |
|
|
gap: 16px; |
|
|
flex: 1; |
|
|
overflow: hidden; |
|
|
padding: 16px 0; |
|
|
} |
|
|
|
|
|
.chat-section { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
background: var(--card); |
|
|
border: 1px solid #1b2331; |
|
|
border-radius: 12px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.system-prompt-section { |
|
|
width: 450px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.card { |
|
|
background: var(--card); |
|
|
border: 1px solid #1b2331; |
|
|
border-radius: 12px; |
|
|
padding: 20px; |
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, .25); |
|
|
} |
|
|
|
|
|
.card h2 { |
|
|
font-size: 16px; |
|
|
margin: 0 0 12px; |
|
|
color: var(--muted); |
|
|
} |
|
|
|
|
|
label { |
|
|
display: block; |
|
|
font-size: 13px; |
|
|
color: var(--muted); |
|
|
margin-bottom: 6px; |
|
|
} |
|
|
|
|
|
textarea { |
|
|
width: 100%; |
|
|
background: #0e141e; |
|
|
color: var(--text); |
|
|
border: 1px solid #1f2a3b; |
|
|
border-radius: 10px; |
|
|
padding: 12px; |
|
|
outline: none; |
|
|
resize: vertical; |
|
|
min-height: 120px; |
|
|
font-family: inherit; |
|
|
} |
|
|
|
|
|
#systemPrompt { |
|
|
min-height: 250px; |
|
|
font-size: 14px; |
|
|
line-height: 1.6; |
|
|
} |
|
|
|
|
|
.chat-messages { |
|
|
flex: 1; |
|
|
overflow-y: auto; |
|
|
padding: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.message { |
|
|
padding: 12px 16px; |
|
|
border-radius: 12px; |
|
|
max-width: 85%; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
.message.user { |
|
|
background: var(--user-msg); |
|
|
align-self: flex-end; |
|
|
margin-left: auto; |
|
|
} |
|
|
|
|
|
.message.assistant { |
|
|
background: var(--assistant-msg); |
|
|
align-self: flex-start; |
|
|
} |
|
|
|
|
|
.message-header { |
|
|
font-size: 12px; |
|
|
color: var(--muted); |
|
|
margin-bottom: 6px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.message-content { |
|
|
white-space: pre-wrap; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.message-actions { |
|
|
margin-top: 8px; |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.message-actions button { |
|
|
padding: 6px 10px; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.chat-input-area { |
|
|
border-top: 1px solid #1b2331; |
|
|
padding: 16px; |
|
|
background: var(--card); |
|
|
} |
|
|
|
|
|
.input-container { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
align-items: flex-end; |
|
|
} |
|
|
|
|
|
.input-box { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.input-box textarea { |
|
|
min-height: 60px; |
|
|
max-height: 150px; |
|
|
resize: none; |
|
|
} |
|
|
|
|
|
button { |
|
|
border: 0; |
|
|
padding: 10px 14px; |
|
|
border-radius: 10px; |
|
|
cursor: pointer; |
|
|
font-weight: 600; |
|
|
font-family: inherit; |
|
|
transition: opacity 0.2s; |
|
|
} |
|
|
|
|
|
button:hover:not(:disabled) { |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
button:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.primary { |
|
|
background: var(--accent); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.ok { |
|
|
background: var(--ok); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.bad { |
|
|
background: var(--bad); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.secondary { |
|
|
background: #2a3544; |
|
|
color: var(--text); |
|
|
} |
|
|
|
|
|
.status { |
|
|
color: var(--muted); |
|
|
font-size: 13px; |
|
|
margin-top: 8px; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
margin-top: 12px; |
|
|
} |
|
|
|
|
|
.btn-clear { |
|
|
background: #2a3544; |
|
|
color: var(--text); |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
@media (max-width: 900px) { |
|
|
.main-content { |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.system-prompt-section { |
|
|
width: 100%; |
|
|
order: -1; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
} |
|
|
|
|
|
.chat-messages::-webkit-scrollbar-track { |
|
|
background: #0e141e; |
|
|
} |
|
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb { |
|
|
background: #2a3544; |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb:hover { |
|
|
background: #3a4554; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<h1>GOXY — Чат и модерация</h1> |
|
|
</div> |
|
|
|
|
|
<div class="main-content"> |
|
|
<div class="chat-section"> |
|
|
<div class="chat-messages" id="chatMessages"> |
|
|
|
|
|
</div> |
|
|
<div class="chat-input-area"> |
|
|
<div class="input-container"> |
|
|
<div class="input-box"> |
|
|
<textarea id="messageInput" placeholder="Введите сообщение..."></textarea> |
|
|
</div> |
|
|
<button id="btnSend" class="primary">Отправить</button> |
|
|
</div> |
|
|
<div class="status" id="status"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="system-prompt-section"> |
|
|
<div class="card"> |
|
|
<h2>Системный промпт</h2> |
|
|
<textarea id="systemPrompt" placeholder="Вы — полезный ассистент..."></textarea> |
|
|
<div class="controls"> |
|
|
<button id="btnClearChat" class="btn-clear">Очистить чат</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const $ = (id) => document.getElementById(id); |
|
|
const systemPromptEl = $("systemPrompt"); |
|
|
const messageInputEl = $("messageInput"); |
|
|
const chatMessagesEl = $("chatMessages"); |
|
|
const statusEl = $("status"); |
|
|
const btnSend = $("btnSend"); |
|
|
const btnClearChat = $("btnClearChat"); |
|
|
|
|
|
|
|
|
let chatHistory = []; |
|
|
let lastResponseId = null; |
|
|
let pendingFeedbackMessageIndex = null; |
|
|
|
|
|
|
|
|
async function loadDefaultPrompt() { |
|
|
try { |
|
|
const res = await fetch('/api/v1/system-prompt-default'); |
|
|
if (res.ok) { |
|
|
const txt = await res.text(); |
|
|
systemPromptEl.value = txt && txt.trim() ? txt.trim() : "Вы — полезный ассистент."; |
|
|
} |
|
|
} catch (e) { |
|
|
systemPromptEl.value = "Вы — полезный ассистент."; |
|
|
} |
|
|
} |
|
|
systemPromptEl.value = "Загружаю системный промпт..."; |
|
|
loadDefaultPrompt(); |
|
|
|
|
|
|
|
|
function addMessage(role, content, responseId = null) { |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = `message ${role}`; |
|
|
|
|
|
const headerDiv = document.createElement('div'); |
|
|
headerDiv.className = 'message-header'; |
|
|
headerDiv.textContent = role === 'user' ? 'Вы' : 'GOXY'; |
|
|
|
|
|
const contentDiv = document.createElement('div'); |
|
|
contentDiv.className = 'message-content'; |
|
|
contentDiv.textContent = content; |
|
|
|
|
|
messageDiv.appendChild(headerDiv); |
|
|
messageDiv.appendChild(contentDiv); |
|
|
|
|
|
|
|
|
if (role === 'assistant' && responseId) { |
|
|
const actionsDiv = document.createElement('div'); |
|
|
actionsDiv.className = 'message-actions'; |
|
|
|
|
|
const btnOk = document.createElement('button'); |
|
|
btnOk.className = 'ok'; |
|
|
btnOk.textContent = '✓ Одобрить'; |
|
|
btnOk.onclick = () => handleFeedback(responseId, 'good', messageDiv); |
|
|
|
|
|
const btnBad = document.createElement('button'); |
|
|
btnBad.className = 'bad'; |
|
|
btnBad.textContent = '✗ Отклонить'; |
|
|
btnBad.onclick = () => handleFeedback(responseId, 'bad', messageDiv); |
|
|
|
|
|
actionsDiv.appendChild(btnOk); |
|
|
actionsDiv.appendChild(btnBad); |
|
|
messageDiv.appendChild(actionsDiv); |
|
|
|
|
|
messageDiv.dataset.responseId = responseId; |
|
|
} |
|
|
|
|
|
chatMessagesEl.appendChild(messageDiv); |
|
|
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; |
|
|
} |
|
|
|
|
|
|
|
|
async function sendMessage() { |
|
|
const message = messageInputEl.value.trim(); |
|
|
if (!message) return; |
|
|
|
|
|
|
|
|
btnSend.disabled = true; |
|
|
messageInputEl.disabled = true; |
|
|
statusEl.textContent = "Отправка..."; |
|
|
|
|
|
|
|
|
addMessage('user', message); |
|
|
|
|
|
|
|
|
chatHistory.push({ role: 'user', content: message }); |
|
|
|
|
|
|
|
|
messageInputEl.value = ''; |
|
|
|
|
|
try { |
|
|
statusEl.textContent = "Генерация ответа..."; |
|
|
|
|
|
const payload = { |
|
|
message: message, |
|
|
chat_history: chatHistory.slice(0, -1), |
|
|
system_prompt: systemPromptEl.value.trim(), |
|
|
task_type: "general", |
|
|
max_length: 200, |
|
|
temperature: 0.7, |
|
|
top_p: 0.9 |
|
|
}; |
|
|
|
|
|
const res = await fetch('/api/v1/generate', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
const data = await res.json(); |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error((data && data.detail && (data.detail.message || data.detail)) || 'Ошибка генерации'); |
|
|
} |
|
|
|
|
|
lastResponseId = data.response_id; |
|
|
const assistantMessage = data.generated_text; |
|
|
|
|
|
|
|
|
addMessage('assistant', assistantMessage, lastResponseId); |
|
|
|
|
|
|
|
|
chatHistory.push({ role: 'assistant', content: assistantMessage }); |
|
|
|
|
|
statusEl.textContent = "Готово"; |
|
|
} catch (e) { |
|
|
statusEl.textContent = "Ошибка: " + (e.message || String(e)); |
|
|
|
|
|
chatHistory.pop(); |
|
|
} finally { |
|
|
btnSend.disabled = false; |
|
|
messageInputEl.disabled = false; |
|
|
messageInputEl.focus(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function handleFeedback(responseId, feedbackType, messageElement) { |
|
|
statusEl.textContent = feedbackType === 'good' ? 'Подтверждение...' : 'Отклонение и перегенерация...'; |
|
|
|
|
|
|
|
|
const buttons = messageElement.querySelectorAll('button'); |
|
|
buttons.forEach(btn => btn.disabled = true); |
|
|
|
|
|
try { |
|
|
|
|
|
const fbRes = await fetch('/api/v1/feedback', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ |
|
|
response_id: responseId, |
|
|
feedback_type: feedbackType, |
|
|
comment: feedbackType === 'good' ? 'Approved by moderator' : 'Rejected by moderator' |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!fbRes.ok) { |
|
|
const err = await fbRes.json().catch(() => ({ detail: 'feedback error' })); |
|
|
throw new Error((err && err.detail && (err.detail.message || err.detail)) || 'Ошибка фидбэка'); |
|
|
} |
|
|
|
|
|
if (feedbackType === 'bad') { |
|
|
|
|
|
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === 'assistant') { |
|
|
chatHistory.pop(); |
|
|
} |
|
|
|
|
|
|
|
|
messageElement.remove(); |
|
|
|
|
|
|
|
|
const lastUserMessage = chatHistory[chatHistory.length - 1]; |
|
|
if (lastUserMessage && lastUserMessage.role === 'user') { |
|
|
|
|
|
await regenerateLastMessage(); |
|
|
} |
|
|
} else { |
|
|
|
|
|
const actionsDiv = messageElement.querySelector('.message-actions'); |
|
|
if (actionsDiv) actionsDiv.remove(); |
|
|
statusEl.textContent = 'Ответ одобрен'; |
|
|
} |
|
|
} catch (e) { |
|
|
statusEl.textContent = "Ошибка: " + (e.message || String(e)); |
|
|
|
|
|
buttons.forEach(btn => btn.disabled = false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function regenerateLastMessage() { |
|
|
if (chatHistory.length === 0) return; |
|
|
|
|
|
const lastMessage = chatHistory[chatHistory.length - 1]; |
|
|
if (lastMessage.role !== 'user') return; |
|
|
|
|
|
statusEl.textContent = "Перегенерация..."; |
|
|
|
|
|
try { |
|
|
const payload = { |
|
|
message: lastMessage.content, |
|
|
chat_history: chatHistory.slice(0, -1), |
|
|
system_prompt: systemPromptEl.value.trim(), |
|
|
task_type: "general", |
|
|
max_length: 200, |
|
|
temperature: 0.7, |
|
|
top_p: 0.9 |
|
|
}; |
|
|
|
|
|
const res = await fetch('/api/v1/generate', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
const data = await res.json(); |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error((data && data.detail && (data.detail.message || data.detail)) || 'Ошибка генерации'); |
|
|
} |
|
|
|
|
|
lastResponseId = data.response_id; |
|
|
const assistantMessage = data.generated_text; |
|
|
|
|
|
|
|
|
addMessage('assistant', assistantMessage, lastResponseId); |
|
|
|
|
|
|
|
|
chatHistory.push({ role: 'assistant', content: assistantMessage }); |
|
|
|
|
|
statusEl.textContent = "Перегенерировано"; |
|
|
} catch (e) { |
|
|
statusEl.textContent = "Ошибка перегенерации: " + (e.message || String(e)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function clearChat() { |
|
|
if (confirm('Очистить весь чат?')) { |
|
|
chatHistory = []; |
|
|
chatMessagesEl.innerHTML = ''; |
|
|
lastResponseId = null; |
|
|
statusEl.textContent = 'Чат очищен'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
btnSend.addEventListener('click', sendMessage); |
|
|
btnClearChat.addEventListener('click', clearChat); |
|
|
|
|
|
messageInputEl.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
sendMessage(); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |