Загрузить файлы в «/»

This commit is contained in:
2026-06-04 16:44:38 +00:00
parent 8947f5b478
commit e654afeb2d
4 changed files with 533 additions and 0 deletions
+383
View File
@@ -0,0 +1,383 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HW Agent</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Onest:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0f14; --surface: #14171f; --surface2: #1c2030;
--border: rgba(255,255,255,0.07); --border2: rgba(255,255,255,0.12);
--text: #e8eaf0; --muted: #6b7280;
--accent: #7c6af5; --accent2: #5b4fd4;
--green: #34d399; --amber: #fbbf24; --red: #f87171;
--mono: 'JetBrains Mono', monospace; --sans: 'Onest', sans-serif;
}
body { background: var(--bg); color: var(--text); font-family: var(--sans); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.topbar { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
.logo { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--accent); display: flex; align-items: center; gap: 8px; }
.logo-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.8)} }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.status-badge { font-family: var(--mono); font-size: 11px; padding: 4px 10px; border-radius: 20px; border: 1px solid var(--border2); color: var(--muted); display: flex; align-items: center; gap: 5px; cursor: pointer; transition: all 0.2s; }
.status-badge:hover { border-color: var(--accent); color: var(--text); }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--red); transition: background 0.3s; }
.status-dot.connected { background: var(--green); }
.new-thread-btn { font-family: var(--mono); font-size: 11px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border2); background: transparent; color: var(--muted); cursor: pointer; transition: all 0.2s; }
.new-thread-btn:hover { background: var(--surface2); color: var(--text); border-color: var(--accent); }
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-title { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; color: var(--muted); padding: 14px 16px 8px; text-transform: uppercase; }
.quick-btn { display: flex; align-items: center; gap: 8px; padding: 9px 16px; font-size: 13px; color: var(--muted); cursor: pointer; transition: all 0.15s; border: none; background: none; text-align: left; width: 100%; font-family: var(--sans); }
.quick-btn:hover { background: var(--surface2); color: var(--text); }
.sidebar-sep { height: 1px; background: var(--border); margin: 8px 0; }
.sidebar-bottom { margin-top: auto; padding: 12px 16px; font-family: var(--mono); font-size: 10px; color: var(--muted); }
.model-tag { background: var(--surface2); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; margin-top: 6px; display: flex; align-items: center; gap: 5px; }
.chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.messages { flex: 1; overflow-y: auto; padding: 24px 28px; display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; }
.messages::-webkit-scrollbar { width: 4px; }
.messages::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; }
.welcome h2 { font-size: 16px; font-weight: 500; color: var(--muted); }
.welcome p { font-size: 13px; color: var(--muted); opacity: 0.6; font-family: var(--mono); }
.suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; justify-content: center; max-width: 500px; }
.suggestion { padding: 6px 14px; background: var(--surface2); border: 1px solid var(--border2); border-radius: 20px; font-size: 12px; color: var(--muted); cursor: pointer; transition: all 0.15s; font-family: var(--mono); }
.suggestion:hover { border-color: var(--accent); color: var(--accent); background: rgba(124,106,245,0.08); }
.msg { display: flex; gap: 12px; animation: fadeUp 0.2s ease-out; }
.msg.user { flex-direction: row-reverse; }
@keyframes fadeUp { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
.avatar { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0; font-family: var(--mono); }
.avatar.ai { background: rgba(124,106,245,0.15); color: var(--accent); border: 1px solid rgba(124,106,245,0.3); }
.avatar.user { background: var(--surface2); color: var(--muted); border: 1px solid var(--border2); }
.bubble { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; font-size: 14px; line-height: 1.6; max-width: 680px; }
.msg.user .bubble { background: var(--surface2); border-color: var(--border2); font-family: var(--mono); font-size: 13px; }
.repo-link { display: inline-flex; align-items: center; gap: 6px; background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.2); border-radius: 6px; padding: 8px 14px; margin: 6px 0; font-family: var(--mono); font-size: 12px; color: var(--green); text-decoration: none; transition: all 0.15s; word-break: break-all; display: block; }
.repo-link:hover { background: rgba(52,211,153,0.15); }
.typing { display: flex; gap: 4px; align-items: center; }
.typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: blink 1.2s ease-in-out infinite; }
.typing span:nth-child(2){animation-delay:0.2s} .typing span:nth-child(3){animation-delay:0.4s}
@keyframes blink { 0%,80%,100%{opacity:0.3;transform:scale(0.8)} 40%{opacity:1;transform:scale(1)} }
.input-area { padding: 16px 28px 20px; border-top: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
.input-wrap { display: flex; gap: 10px; align-items: flex-end; background: var(--bg); border: 1px solid var(--border2); border-radius: 12px; padding: 10px 12px 10px 16px; transition: border-color 0.2s; }
.input-wrap:focus-within { border-color: var(--accent); }
textarea { flex: 1; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.5; resize: none; max-height: 120px; min-height: 20px; }
textarea::placeholder { color: var(--muted); }
.send-btn { width: 32px; height: 32px; border-radius: 8px; background: var(--accent); border: none; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; flex-shrink: 0; font-size: 14px; }
.send-btn:hover { background: var(--accent2); transform: scale(1.05); }
.send-btn:disabled { background: var(--surface2); cursor: not-allowed; transform: none; }
.input-hint { font-family: var(--mono); font-size: 10px; color: var(--muted); margin-top: 6px; padding: 0 4px; opacity: 0.5; }
/* Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(4px); }
.modal-overlay.hidden { display: none; }
.modal { background: var(--surface); border: 1px solid var(--border2); border-radius: 14px; padding: 24px; width: 440px; max-width: 90vw; }
.modal h3 { font-family: var(--mono); font-size: 14px; color: var(--accent); margin-bottom: 16px; }
.modal label { font-size: 12px; color: var(--muted); font-family: var(--mono); display: block; margin-bottom: 6px; }
.modal input, .modal select { width: 100%; background: var(--bg); border: 1px solid var(--border2); border-radius: 8px; padding: 9px 12px; color: var(--text); font-family: var(--mono); font-size: 13px; outline: none; margin-bottom: 14px; }
.modal input:focus, .modal select:focus { border-color: var(--accent); }
.modal select option { background: var(--surface); }
.modal-btns { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.btn-cancel { padding: 8px 16px; background: transparent; border: 1px solid var(--border2); border-radius: 8px; color: var(--muted); font-family: var(--mono); font-size: 12px; cursor: pointer; }
.btn-cancel:hover { border-color: var(--border2); color: var(--text); }
.btn-ok { padding: 8px 20px; background: var(--accent); border: none; border-radius: 8px; color: white; font-family: var(--mono); font-size: 12px; cursor: pointer; }
.btn-ok:hover { background: var(--accent2); }
</style>
</head>
<body>
<!-- Modal -->
<div class="modal-overlay hidden" id="modalOverlay">
<div class="modal">
<h3 id="modalTitle">⚡ решить задание</h3>
<label>название задания</label>
<input type="text" id="modalInput" placeholder="например: текстовая игра на основе llm + interrupt" />
<div id="reviseExtra" style="display:none">
<label>что исправить (необязательно — агент прочитает комментарий сам)</label>
<input type="text" id="modalComment" placeholder="или оставь пустым" />
</div>
<div class="modal-btns">
<button class="btn-cancel" onclick="closeModal()">отмена</button>
<button class="btn-ok" onclick="submitModal()">отправить ▶</button>
</div>
</div>
</div>
<div class="topbar">
<div class="logo"><div class="logo-dot"></div>hw_agent</div>
<div class="topbar-right">
<div class="status-badge" id="statusBadge" onclick="checkConnection()">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">connecting...</span>
</div>
<button class="new-thread-btn" onclick="newThread()">+ new thread</button>
</div>
</div>
<div class="main">
<div class="sidebar">
<div class="sidebar-title">команды</div>
<button class="quick-btn" onclick="quickSend('покажи список всех заданий курса KFU-26-1')">📋 список заданий</button>
<button class="quick-btn" onclick="openModal('solve')">⚡ решить задание</button>
<button class="quick-btn" onclick="openModal('revise')">✏️ переделать по комменту</button>
<div class="sidebar-sep"></div>
<div class="sidebar-title">другое</div>
<button class="quick-btn" onclick="quickSend('покажи статус всех заданий')">📊 статус заданий</button>
<button class="quick-btn" onclick="quickSend('какие инструменты у тебя есть?')">🔧 инструменты</button>
<div class="sidebar-bottom">
<div style="opacity:0.5">модель</div>
<div class="model-tag"><span style="color:var(--accent)"></span><span id="modelName">gpt-oss-20b</span></div>
<div style="margin-top:8px;opacity:0.5">api</div>
<div class="model-tag" style="font-size:10px">localhost:2024</div>
</div>
</div>
<div class="chat-area">
<div class="messages" id="messages">
<div class="welcome" id="welcome">
<div style="font-size:36px;opacity:0.3"></div>
<h2>homework agent</h2>
<p>langgraph @ localhost:2024</p>
<div class="suggestions">
<div class="suggestion" onclick="quickSend('покажи список заданий курса KFU-26-1')">список заданий</div>
<div class="suggestion" onclick="openModal('solve')">решить задание</div>
<div class="suggestion" onclick="openModal('revise')">переделать по комменту</div>
</div>
</div>
</div>
<div class="input-area">
<div class="input-wrap">
<textarea id="input" placeholder="напиши агенту..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()"></button>
</div>
<div class="input-hint">enter — отправить · shift+enter — перенос строки</div>
</div>
</div>
</div>
<script>
const API = 'http://127.0.0.1:2024';
const ASSISTANT_ID = '46958879-389b-529f-bca9-2baa098385d8';
let threadId = null;
let isRunning = false;
let modalMode = 'solve';
// Modal
function openModal(mode) {
modalMode = mode;
document.getElementById('modalTitle').textContent = mode === 'solve' ? '⚡ решить задание' : '✏️ переделать задание';
document.getElementById('modalInput').placeholder = mode === 'solve'
? 'например: Агент с RAG-памятью'
: 'например: MCP-сервер для управления памятью агента';
document.getElementById('reviseExtra').style.display = mode === 'revise' ? 'block' : 'none';
document.getElementById('modalInput').value = '';
document.getElementById('modalComment').value = '';
document.getElementById('modalOverlay').classList.remove('hidden');
setTimeout(() => document.getElementById('modalInput').focus(), 50);
}
function closeModal() {
document.getElementById('modalOverlay').classList.add('hidden');
}
function submitModal() {
const name = document.getElementById('modalInput').value.trim();
if (!name) { document.getElementById('modalInput').focus(); return; }
closeModal();
if (modalMode === 'solve') {
quickSend(`реши задание "${name}"`);
} else {
const comment = document.getElementById('modalComment').value.trim();
if (comment) {
quickSend(`переделай задание "${name}" по комментарию: ${comment}`);
} else {
quickSend(`переделай задание "${name}" по комментарию преподавателя`);
}
}
}
document.getElementById('modalInput').addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); submitModal(); }
if (e.key === 'Escape') closeModal();
});
document.getElementById('modalOverlay').addEventListener('click', e => {
if (e.target === e.currentTarget) closeModal();
});
// Connection
async function checkConnection() {
try {
const r = await fetch(`${API}/assistants/${ASSISTANT_ID}`, {signal: AbortSignal.timeout(3000)});
const dot = document.getElementById('statusDot');
const txt = document.getElementById('statusText');
if (r.ok) {
dot.className = 'status-dot connected';
txt.textContent = 'connected';
} else {
dot.className = 'status-dot';
txt.textContent = 'error ' + r.status;
}
} catch(e) {
document.getElementById('statusDot').className = 'status-dot';
document.getElementById('statusText').textContent = 'offline';
}
}
async function newThread() {
try {
const r = await fetch(`${API}/threads`, {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'});
if (r.ok) { const d = await r.json(); threadId = d.thread_id; }
else threadId = 'local-' + Date.now();
} catch { threadId = 'local-' + Date.now(); }
document.getElementById('messages').innerHTML = '';
}
// Messages
function hideWelcome() {
const w = document.getElementById('welcome');
if (w) w.remove();
}
function addMessage(role, html) {
hideWelcome();
const msgs = document.getElementById('messages');
const div = document.createElement('div');
div.className = `msg ${role}`;
const label = role === 'user' ? 'you' : 'ai';
div.innerHTML = `<div class="avatar ${role}">${label}</div><div class="bubble">${html}</div>`;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
return div.querySelector('.bubble');
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function formatAiText(text) {
// Linkify git.brojs.ru URLs
return escHtml(text).replace(
/https:\/\/git\.brojs\.ru\/\S+/g,
url => `<a class="repo-link" href="${url}" target="_blank">🔗 ${url}</a>`
).replace(/\n/g, '<br>');
}
// Send
async function sendMessage() {
const input = document.getElementById('input');
const text = input.value.trim();
if (!text || isRunning) return;
if (!threadId) await newThread();
input.value = '';
autoResize(input);
isRunning = true;
document.getElementById('sendBtn').disabled = true;
addMessage('user', escHtml(text));
const aiBubble = addMessage('ai', '<div class="typing"><span></span><span></span><span></span></div>');
try {
const resp = await fetch(`${API}/threads/${threadId}/runs/stream`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
assistant_id: ASSISTANT_ID,
input: {messages: [{role: 'user', content: text}]},
stream_mode: 'values'
})
});
if (!resp.ok) {
aiBubble.innerHTML = `<span style="color:var(--red)">❌ HTTP ${resp.status} — убедись что langgraph dev запущен</span>`;
isRunning = false;
document.getElementById('sendBtn').disabled = false;
return;
}
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
let lastAiText = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buf += dec.decode(value, {stream: true});
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const raw = line.slice(6).trim();
if (!raw || raw === '[DONE]') continue;
try {
const ev = JSON.parse(raw);
// values mode: ev is the state object with messages array
const msgs = ev.messages || (Array.isArray(ev) ? ev : null);
if (msgs) {
for (let i = msgs.length - 1; i >= 0; i--) {
const m = msgs[i];
const role = m.type || m.role;
if (role === 'ai' || role === 'assistant') {
const c = m.content;
if (typeof c === 'string' && c.trim()) { lastAiText = c; break; }
if (Array.isArray(c)) {
const t = c.filter(x => x.type === 'text').map(x => x.text).join('');
if (t.trim()) { lastAiText = t; break; }
}
}
}
}
// Show live update
if (lastAiText) aiBubble.innerHTML = formatAiText(lastAiText);
} catch {}
}
}
if (!lastAiText) aiBubble.innerHTML = '<span style="color:var(--muted)">агент не вернул ответ — проверь логи в терминале</span>';
else aiBubble.innerHTML = formatAiText(lastAiText);
} catch(e) {
aiBubble.innerHTML = `<span style="color:var(--red)">❌ ${escHtml(e.message)}</span><br><span style="color:var(--muted);font-family:var(--mono);font-size:12px">запусти: langgraph dev</span>`;
}
isRunning = false;
document.getElementById('sendBtn').disabled = false;
document.getElementById('messages').scrollTop = 99999;
}
function quickSend(text) {
document.getElementById('input').value = text;
autoResize(document.getElementById('input'));
sendMessage();
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
checkConnection();
newThread();
setInterval(checkConnection, 15000);
</script>
</body>
</html>