lasagnakanada
Deploy GOXY ML Service to HuggingFace Space
3d6cc8a
<!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;
}
}
/* Scrollbar styling */
.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">
<!-- Messages will be inserted here -->
</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");
// Chat history stored in memory
let chatHistory = [];
let lastResponseId = null;
let pendingFeedbackMessageIndex = null;
// Load default system prompt
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();
// Add message to chat UI
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);
// Add moderation buttons for assistant messages
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;
}
// Send message
async function sendMessage() {
const message = messageInputEl.value.trim();
if (!message) return;
// Disable input while processing
btnSend.disabled = true;
messageInputEl.disabled = true;
statusEl.textContent = "Отправка...";
// Add user message to UI
addMessage('user', message);
// Add user message to history
chatHistory.push({ role: 'user', content: message });
// Clear input
messageInputEl.value = '';
try {
statusEl.textContent = "Генерация ответа...";
const payload = {
message: message,
chat_history: chatHistory.slice(0, -1), // Send history without current message
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;
// Add assistant message to UI
addMessage('assistant', assistantMessage, lastResponseId);
// Add assistant message to history
chatHistory.push({ role: 'assistant', content: assistantMessage });
statusEl.textContent = "Готово";
} catch (e) {
statusEl.textContent = "Ошибка: " + (e.message || String(e));
// Remove last user message from history on error
chatHistory.pop();
} finally {
btnSend.disabled = false;
messageInputEl.disabled = false;
messageInputEl.focus();
}
}
// Handle feedback (approve/reject)
async function handleFeedback(responseId, feedbackType, messageElement) {
statusEl.textContent = feedbackType === 'good' ? 'Подтверждение...' : 'Отклонение и перегенерация...';
// Disable buttons
const buttons = messageElement.querySelectorAll('button');
buttons.forEach(btn => btn.disabled = true);
try {
// Send feedback
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') {
// Remove last assistant message from history and regenerate
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === 'assistant') {
chatHistory.pop();
}
// Remove message from UI
messageElement.remove();
// Get last user message
const lastUserMessage = chatHistory[chatHistory.length - 1];
if (lastUserMessage && lastUserMessage.role === 'user') {
// Regenerate
await regenerateLastMessage();
}
} else {
// Remove action buttons on approval
const actionsDiv = messageElement.querySelector('.message-actions');
if (actionsDiv) actionsDiv.remove();
statusEl.textContent = 'Ответ одобрен';
}
} catch (e) {
statusEl.textContent = "Ошибка: " + (e.message || String(e));
// Re-enable buttons on error
buttons.forEach(btn => btn.disabled = false);
}
}
// Regenerate last message
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;
// Add new assistant message to UI
addMessage('assistant', assistantMessage, lastResponseId);
// Add assistant message to history
chatHistory.push({ role: 'assistant', content: assistantMessage });
statusEl.textContent = "Перегенерировано";
} catch (e) {
statusEl.textContent = "Ошибка перегенерации: " + (e.message || String(e));
}
}
// Clear chat
function clearChat() {
if (confirm('Очистить весь чат?')) {
chatHistory = [];
chatMessagesEl.innerHTML = '';
lastResponseId = null;
statusEl.textContent = 'Чат очищен';
}
}
// Event listeners
btnSend.addEventListener('click', sendMessage);
btnClearChat.addEventListener('click', clearChat);
messageInputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
</body>
</html>