795 lines
28 KiB
HTML
795 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>ducklm runtime test chat</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f5f1e8;
|
||
--panel: #fffdf7;
|
||
--border: #c9bba4;
|
||
--text: #1d1a16;
|
||
--accent: #195c4b;
|
||
--muted: #6f6659;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: Georgia, "Iowan Old Style", serif;
|
||
background: radial-gradient(circle at top, #fff7df 0%, var(--bg) 60%);
|
||
color: var(--text);
|
||
}
|
||
main {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 32px 20px 48px;
|
||
}
|
||
h1 {
|
||
margin: 0 0 8px;
|
||
font-size: 2.2rem;
|
||
}
|
||
p {
|
||
color: var(--muted);
|
||
margin: 0 0 24px;
|
||
}
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr 350px;
|
||
gap: 20px;
|
||
}
|
||
.panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 18px;
|
||
padding: 18px;
|
||
box-shadow: 0 14px 30px rgba(35, 28, 17, 0.08);
|
||
max-width: 100%;
|
||
}
|
||
textarea, input {
|
||
width: 100%;
|
||
border: 1px solid var(--border);
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
font: inherit;
|
||
}
|
||
textarea {
|
||
min-height: 120px;
|
||
resize: vertical;
|
||
}
|
||
button {
|
||
margin-top: 12px;
|
||
background: var(--accent);
|
||
color: white;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 10px 18px;
|
||
font: inherit;
|
||
cursor: pointer;
|
||
}
|
||
button.secondary {
|
||
background: #ffffff;
|
||
color: var(--accent);
|
||
border: 1px solid var(--border);
|
||
}
|
||
button.danger {
|
||
background: #b42318;
|
||
}
|
||
.messages, .runtime-log, .events {
|
||
display: grid;
|
||
gap: 12px;
|
||
max-height: 520px;
|
||
overflow: auto;
|
||
width: 100%;
|
||
}
|
||
.messages {
|
||
width: 100%;
|
||
}
|
||
.runtime-log {
|
||
margin-top: 12px;
|
||
}
|
||
.events {
|
||
max-height: 70vh;
|
||
}
|
||
.bubble, .event, .runtime-entry {
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 12px;
|
||
background: white;
|
||
}
|
||
.event code {
|
||
color: var(--accent);
|
||
}
|
||
.row {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
.feedback-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.panel-header button {
|
||
margin-top: 0;
|
||
}
|
||
dialog {
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 0;
|
||
width: min(560px, calc(100vw - 32px));
|
||
color: var(--text);
|
||
}
|
||
dialog::backdrop {
|
||
background: rgba(20, 18, 15, 0.4);
|
||
}
|
||
dialog.wide {
|
||
width: min(920px, calc(100vw - 32px));
|
||
}
|
||
.modal-body {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 18px;
|
||
background: var(--panel);
|
||
}
|
||
select {
|
||
width: 100%;
|
||
border: 1px solid var(--border);
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 10px;
|
||
font: inherit;
|
||
}
|
||
label {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
label.inline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
label.inline input {
|
||
width: auto;
|
||
}
|
||
@media (max-width: 860px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>ducklm runtime test chat</h1>
|
||
<p>Thin browser client for checking task submission, tool execution and event replay.</p>
|
||
<div class="layout">
|
||
<section class="panel">
|
||
<div class="messages" id="messages"></div>
|
||
<div class="row">
|
||
<textarea id="prompt" placeholder="Опиши задачу..." style="min-height: 60px;"></textarea>
|
||
<button id="sendBtn">Отправить</button>
|
||
</div>
|
||
</section>
|
||
<aside class="panel">
|
||
<div class="panel-header">
|
||
<strong>Runtime</strong>
|
||
<button class="secondary" id="eventsBtn">Events</button>
|
||
</div>
|
||
<div class="runtime-log" id="runtimeLog"></div>
|
||
</aside>
|
||
</div>
|
||
<dialog id="eventsDialog" class="wide">
|
||
<div class="modal-body">
|
||
<div class="panel-header">
|
||
<strong>Events</strong>
|
||
<div>
|
||
<button class="secondary" id="refreshEventsBtn">Обновить</button>
|
||
<button class="secondary" id="closeEventsBtn">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
<div class="events" id="events"></div>
|
||
</div>
|
||
</dialog>
|
||
<dialog id="feedbackDialog">
|
||
<form method="dialog" class="modal-body" id="feedbackForm">
|
||
<strong>Что было неверно?</strong>
|
||
<label>
|
||
Тип ошибки
|
||
<select id="feedbackType">
|
||
<option value="misunderstood_task">Неправильно понял задачу</option>
|
||
<option value="wrong_tool">Выбрал не тот инструмент</option>
|
||
<option value="wrong_command">Выполнил не ту команду</option>
|
||
<option value="should_have_checked">Ответил без проверки</option>
|
||
<option value="hallucination">Выдумал факт</option>
|
||
<option value="incomplete">Неполный ответ</option>
|
||
<option value="unsafe">Опасное действие</option>
|
||
<option value="bad_format">Плохой формат ответа</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Критичность
|
||
<select id="feedbackSeverity">
|
||
<option value="minor">Мелкая ошибка</option>
|
||
<option value="major" selected>Существенная ошибка</option>
|
||
<option value="critical">Критическая ошибка</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Комментарий
|
||
<textarea id="feedbackText" placeholder="Что именно было неверно?"></textarea>
|
||
</label>
|
||
<label>
|
||
Как должно было быть
|
||
<textarea id="feedbackCorrection" placeholder="Корректировка или желаемое поведение"></textarea>
|
||
</label>
|
||
<label class="inline">
|
||
<input type="checkbox" id="feedbackRemember" checked />
|
||
Запомнить для похожих задач
|
||
</label>
|
||
<label class="inline">
|
||
<input type="checkbox" id="feedbackRetry" />
|
||
Исправить ответ сейчас
|
||
</label>
|
||
<div>
|
||
<button id="submitFeedbackBtn" value="submit">Отправить</button>
|
||
<button class="secondary" value="cancel">Отмена</button>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
</main>
|
||
<script>
|
||
const messages = document.getElementById("messages");
|
||
const events = document.getElementById("events");
|
||
const runtimeLog = document.getElementById("runtimeLog");
|
||
const promptEl = document.getElementById("prompt");
|
||
const sendBtn = document.getElementById("sendBtn");
|
||
const eventsBtn = document.getElementById("eventsBtn");
|
||
const eventsDialog = document.getElementById("eventsDialog");
|
||
const refreshEventsBtn = document.getElementById("refreshEventsBtn");
|
||
const closeEventsBtn = document.getElementById("closeEventsBtn");
|
||
let lastPermissionRequest = null;
|
||
let lastSecretRequest = null;
|
||
let lastPasswordRequest = null;
|
||
const seenEvents = new Set();
|
||
let activePermissionBubble = null;
|
||
let activeSecretBubble = null;
|
||
let activePasswordBubble = null;
|
||
const allEvents = [];
|
||
let currentTaskId = null;
|
||
let currentSessionId = "web-session";
|
||
let pendingFeedback = null;
|
||
const feedbackDialog = document.getElementById("feedbackDialog");
|
||
const feedbackForm = document.getElementById("feedbackForm");
|
||
const feedbackType = document.getElementById("feedbackType");
|
||
const feedbackSeverity = document.getElementById("feedbackSeverity");
|
||
const feedbackText = document.getElementById("feedbackText");
|
||
const feedbackCorrection = document.getElementById("feedbackCorrection");
|
||
const feedbackRemember = document.getElementById("feedbackRemember");
|
||
const feedbackRetry = document.getElementById("feedbackRetry");
|
||
|
||
function addBubble(title, body) {
|
||
const el = document.createElement("div");
|
||
el.className = "bubble";
|
||
el.innerHTML = `<strong>${title}</strong><div>${body}</div>`;
|
||
messages.appendChild(el);
|
||
messages.scrollTop = messages.scrollHeight;
|
||
return el;
|
||
}
|
||
|
||
function addRuntimeEntry(title, body) {
|
||
const el = document.createElement("div");
|
||
el.className = "runtime-entry";
|
||
el.innerHTML = `<strong>${title}</strong><div>${body}</div>`;
|
||
runtimeLog.appendChild(el);
|
||
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||
return el;
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">");
|
||
}
|
||
|
||
function addSystemMessage(title, text) {
|
||
return addRuntimeEntry(title, `<div>${escapeHtml(text)}</div>`);
|
||
}
|
||
|
||
function addJsonBubble(title, data) {
|
||
return addBubble(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
||
}
|
||
|
||
function addRuntimeJson(title, data) {
|
||
return addRuntimeEntry(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
||
}
|
||
|
||
function addFeedbackControls(bubble, answerText) {
|
||
if (!currentTaskId || !bubble) return;
|
||
const actions = document.createElement("div");
|
||
actions.className = "feedback-actions";
|
||
actions.innerHTML = `
|
||
<button class="secondary" data-kind="correct">Верно</button>
|
||
<button class="danger" data-kind="wrong">Неверно</button>
|
||
`;
|
||
actions.querySelector('[data-kind="correct"]').addEventListener("click", async () => {
|
||
const data = await submitFeedback({
|
||
feedback_type: "correct",
|
||
severity: "minor",
|
||
feedback: "User marked response as correct.",
|
||
correction: "",
|
||
remember: true,
|
||
retry: false,
|
||
assistant_answer: answerText
|
||
});
|
||
if (data?.status === "ok") {
|
||
actions.remove();
|
||
addSystemMessage("Feedback", "Оценка сохранена.");
|
||
}
|
||
});
|
||
actions.querySelector('[data-kind="wrong"]').addEventListener("click", () => {
|
||
pendingFeedback = { answerText, actions };
|
||
feedbackText.value = "";
|
||
feedbackCorrection.value = "";
|
||
feedbackType.value = "misunderstood_task";
|
||
feedbackSeverity.value = "major";
|
||
feedbackRemember.checked = true;
|
||
feedbackRetry.checked = false;
|
||
feedbackDialog.showModal();
|
||
});
|
||
bubble.appendChild(actions);
|
||
}
|
||
|
||
function renderRuntimeResult(result, status) {
|
||
if (!result) {
|
||
addSystemMessage("Runtime", "No result returned.");
|
||
return;
|
||
}
|
||
if (status === "awaiting_permission" && result.permission_request) {
|
||
lastPermissionRequest = result.permission_request;
|
||
renderPermissionControls(result.permission_request);
|
||
return;
|
||
}
|
||
if (status === "awaiting_input" && result.secret_request) {
|
||
addSystemMessage("System", result.secret_request.prompt || "Secret input required.");
|
||
lastSecretRequest = result.secret_request;
|
||
renderSecretControls(result.secret_request);
|
||
return;
|
||
}
|
||
if (status === "awaiting_password" && result.needs_sudo) {
|
||
lastPasswordRequest = result;
|
||
renderPasswordControls(result);
|
||
return;
|
||
}
|
||
if (status === "awaiting_permission" && result.error) {
|
||
addSystemMessage("System", result.error);
|
||
return;
|
||
}
|
||
if (result.message && !result.step_results) {
|
||
const bubble = addSystemMessage("Runtime", result.message);
|
||
addFeedbackControls(bubble, result.message);
|
||
}
|
||
if (result.step_results && Array.isArray(result.step_results)) {
|
||
for (const step of result.step_results) {
|
||
const toolResult = step.result?.result || step.result;
|
||
if (toolResult && toolResult.output) {
|
||
const bubble = addBubble("💻", escapeHtml(toolResult.output));
|
||
addFeedbackControls(bubble, String(toolResult.output));
|
||
} else if (toolResult && toolResult.error) {
|
||
addSystemMessage("❌", toolResult.error);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (typeof result.output === "string") {
|
||
const bubble = addBubble("Runtime", `<pre>${escapeHtml(result.output)}</pre>`);
|
||
addFeedbackControls(bubble, result.output);
|
||
return;
|
||
}
|
||
const bubble = addJsonBubble("Runtime", result);
|
||
addFeedbackControls(bubble, JSON.stringify(result));
|
||
}
|
||
|
||
function addEvent(event) {
|
||
const eventKey = `${event.task_id || "na"}:${event.sequence || JSON.stringify(event)}`;
|
||
if (seenEvents.has(eventKey)) {
|
||
return;
|
||
}
|
||
seenEvents.add(eventKey);
|
||
allEvents.push(event);
|
||
renderEvents(allEvents);
|
||
if (event.type === "permission_requested") {
|
||
lastPermissionRequest = event.payload;
|
||
renderPermissionControls(event.payload);
|
||
}
|
||
if (event.type === "permission_resolved" || event.type === "task_failed" || event.type === "task_completed") {
|
||
clearPermissionControls();
|
||
}
|
||
if (event.type === "orchestrator_result") {
|
||
const p = event.payload || {};
|
||
const directive = p.directive || {};
|
||
if (directive.type === "respond") {
|
||
const text = directive.payload?.text || directive.payload?.message || "";
|
||
if (text) {
|
||
const bubble = addBubble("🤖", escapeHtml(text));
|
||
addFeedbackControls(bubble, text);
|
||
}
|
||
}
|
||
}
|
||
if (event.type === "task_completed") {
|
||
const p = event.payload || {};
|
||
const execResult = p.execution_result || {};
|
||
if (execResult.step_results && Array.isArray(execResult.step_results)) {
|
||
for (const step of execResult.step_results) {
|
||
const toolResult = step.result?.result || step.result;
|
||
if (toolResult && toolResult.output) {
|
||
const bubble = addBubble("💻", escapeHtml(toolResult.output));
|
||
addFeedbackControls(bubble, String(toolResult.output));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (event.type === "secret_requested") {
|
||
lastSecretRequest = event.payload;
|
||
renderSecretControls(event.payload);
|
||
}
|
||
if (event.type === "task_completed" || event.type === "task_failed") {
|
||
clearSecretControls();
|
||
clearPasswordControls();
|
||
}
|
||
}
|
||
|
||
function renderEvents(eventList) {
|
||
events.innerHTML = "";
|
||
for (const event of eventList) {
|
||
const el = document.createElement("div");
|
||
el.className = "event";
|
||
el.innerHTML = `<div><code>${escapeHtml(event.type)}</code> <span style="color:var(--muted)">${escapeHtml(event.task_id || "")}</span></div><pre>${escapeHtml(JSON.stringify(event.payload ?? event, null, 2))}</pre>`;
|
||
events.appendChild(el);
|
||
}
|
||
events.scrollTop = events.scrollHeight;
|
||
}
|
||
|
||
async function loadRecentEvents() {
|
||
const response = await fetch("/events?limit=1000");
|
||
const data = await response.json();
|
||
if (!data || !Array.isArray(data.events)) {
|
||
return;
|
||
}
|
||
allEvents.length = 0;
|
||
seenEvents.clear();
|
||
for (const event of data.events) {
|
||
const eventKey = `${event.task_id || "na"}:${event.sequence || JSON.stringify(event)}`;
|
||
seenEvents.add(eventKey);
|
||
allEvents.push(event);
|
||
}
|
||
renderEvents(allEvents);
|
||
}
|
||
|
||
function renderPermissionControls(request) {
|
||
clearPermissionControls();
|
||
const command = request.command || JSON.stringify(request);
|
||
const el = document.createElement("div");
|
||
el.className = "bubble";
|
||
el.innerHTML = `
|
||
<strong>⚠️ Требуется разрешение</strong>
|
||
<div style="margin: 8px 0; padding: 8px; background: #fff3cd; border-radius: 8px; font-family: monospace; font-size: 12px;">${escapeHtml(command)}</div>
|
||
<div>
|
||
<button data-decision="allow_once">Разрешить</button>
|
||
${request.allow_always !== false ? '<button data-decision="allow_always">Разрешить навсегда</button>' : ''}
|
||
<button data-decision="deny" style="background: #dc3545;">Запретить</button>
|
||
</div>
|
||
`;
|
||
el.querySelectorAll("button").forEach((button) => {
|
||
button.addEventListener("click", async () => {
|
||
if (button.disabled) return;
|
||
await resolvePermission(button.dataset.decision);
|
||
});
|
||
});
|
||
activePermissionBubble = el;
|
||
runtimeLog.appendChild(el);
|
||
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||
}
|
||
|
||
function clearPermissionControls() {
|
||
if (activePermissionBubble) {
|
||
activePermissionBubble.remove();
|
||
activePermissionBubble = null;
|
||
}
|
||
}
|
||
|
||
function renderSecretControls(request) {
|
||
clearSecretControls();
|
||
const el = document.createElement("div");
|
||
el.className = "bubble";
|
||
el.innerHTML = `
|
||
<strong>Secret required</strong>
|
||
<div>${escapeHtml(request.prompt)}</div>
|
||
<div style="margin-top:8px;color:var(--muted)">Command: <code>${escapeHtml(request.command || "")}</code></div>
|
||
<input type="password" placeholder="Enter secret" />
|
||
<button>Submit secret</button>
|
||
`;
|
||
const input = el.querySelector("input");
|
||
const submitBtn = el.querySelector("button");
|
||
input.addEventListener("keydown", async (event) => {
|
||
if (event.key === "Enter") {
|
||
event.preventDefault();
|
||
await resolveSecret(input.value);
|
||
}
|
||
});
|
||
submitBtn.addEventListener("click", async () => {
|
||
await resolveSecret(input.value);
|
||
});
|
||
activeSecretBubble = el;
|
||
runtimeLog.appendChild(el);
|
||
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||
input.focus();
|
||
}
|
||
|
||
function clearSecretControls() {
|
||
if (activeSecretBubble) {
|
||
activeSecretBubble.remove();
|
||
activeSecretBubble = null;
|
||
}
|
||
}
|
||
|
||
function renderPasswordControls(request) {
|
||
clearPasswordControls();
|
||
const command = request.command || "unknown command";
|
||
const el = document.createElement("div");
|
||
el.className = "bubble";
|
||
el.innerHTML = `
|
||
<strong>🔐 Требуется пароль sudo</strong>
|
||
<div style="margin: 8px 0; padding: 8px; background: #fff3cd; border-radius: 8px; font-family: monospace; font-size: 12px;">sudo ${escapeHtml(command)}</div>
|
||
<div style="margin-top:8px;">
|
||
<input type="password" placeholder="Введите пароль sudo" />
|
||
<button>Выполнить с sudo</button>
|
||
</div>
|
||
`;
|
||
const input = el.querySelector("input");
|
||
const button = el.querySelector("button");
|
||
input.addEventListener("keydown", async (event) => {
|
||
if (event.key === "Enter") {
|
||
event.preventDefault();
|
||
await resolvePassword(input.value);
|
||
}
|
||
});
|
||
button.addEventListener("click", async () => {
|
||
await resolvePassword(input.value);
|
||
});
|
||
activePasswordBubble = el;
|
||
runtimeLog.appendChild(el);
|
||
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||
input.focus();
|
||
}
|
||
|
||
function clearPasswordControls() {
|
||
if (activePasswordBubble) {
|
||
activePasswordBubble.remove();
|
||
activePasswordBubble = null;
|
||
}
|
||
}
|
||
|
||
async function resolvePermission(decision) {
|
||
if (!lastPermissionRequest) {
|
||
addBubble("Client error", "No pending permission request.");
|
||
return;
|
||
}
|
||
if (activePermissionBubble) {
|
||
activePermissionBubble.querySelectorAll("button").forEach((button) => {
|
||
button.disabled = true;
|
||
});
|
||
}
|
||
const response = await fetch("/permissions/resolve", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
task_id: lastPermissionRequest.task_id,
|
||
decision
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
clearPermissionControls();
|
||
if (!data || !data.status) {
|
||
addSystemMessage("Error", "Failed to resolve permission");
|
||
return;
|
||
}
|
||
if (decision === "deny") {
|
||
addSystemMessage("Permission", "Permission denied.");
|
||
} else if (decision === "allow_always") {
|
||
addSystemMessage("Permission", "Permission granted permanently for this pattern.");
|
||
} else {
|
||
addSystemMessage("Permission", "Permission granted once.");
|
||
}
|
||
if (data.events && Array.isArray(data.events)) {
|
||
data.events.forEach(addEvent);
|
||
}
|
||
renderRuntimeResult(data.result, data.status);
|
||
}
|
||
|
||
async function resolveSecret(secret) {
|
||
if (!lastSecretRequest) {
|
||
addBubble("Client error", "No pending secret request.");
|
||
return;
|
||
}
|
||
if (!secret) {
|
||
addSystemMessage("System", "Secret cannot be empty.");
|
||
return;
|
||
}
|
||
if (activeSecretBubble) {
|
||
const input = activeSecretBubble.querySelector("input");
|
||
const button = activeSecretBubble.querySelector("button");
|
||
if (input) input.disabled = true;
|
||
if (button) button.disabled = true;
|
||
}
|
||
clearSecretControls();
|
||
addSystemMessage("System", "Secret submitted. Waiting for command result.");
|
||
const response = await fetch("/secrets/resolve", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
task_id: lastSecretRequest.task_id,
|
||
secret
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
data.events.forEach(addEvent);
|
||
renderRuntimeResult(data.result, data.status);
|
||
}
|
||
|
||
async function resolvePassword(password) {
|
||
if (!lastPasswordRequest) {
|
||
addBubble("Client error", "No pending password request.");
|
||
return;
|
||
}
|
||
if (!password) {
|
||
addSystemMessage("System", "Пароль не может быть пустым.");
|
||
return;
|
||
}
|
||
if (activePasswordBubble) {
|
||
const input = activePasswordBubble.querySelector("input");
|
||
const button = activePasswordBubble.querySelector("button");
|
||
if (input) input.disabled = true;
|
||
if (button) button.disabled = true;
|
||
}
|
||
clearPasswordControls();
|
||
addSystemMessage("System", "Выполняю команду с sudo...");
|
||
const response = await fetch("/password/resolve", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
task_id: lastPasswordRequest.task_id,
|
||
password
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
data.events.forEach(addEvent);
|
||
renderRuntimeResult(data.result, data.status);
|
||
}
|
||
|
||
async function sendTask() {
|
||
const taskId = "web-" + Date.now();
|
||
const body = {
|
||
input: promptEl.value || "browser task",
|
||
task_id: taskId,
|
||
session_id: currentSessionId,
|
||
context: {}
|
||
};
|
||
if (!promptEl.value.trim()) return;
|
||
addBubble("User", promptEl.value);
|
||
promptEl.value = "";
|
||
|
||
const response = await fetch("/chat", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body)
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (!data || !data.events) {
|
||
addSystemMessage("Error", "Invalid response from server");
|
||
return;
|
||
}
|
||
|
||
lastPermissionRequest = null;
|
||
lastSecretRequest = null;
|
||
lastPasswordRequest = null;
|
||
currentTaskId = data.task_id || taskId;
|
||
currentSessionId = body.session_id;
|
||
clearPermissionControls();
|
||
clearSecretControls();
|
||
clearPasswordControls();
|
||
data.events.forEach(addEvent);
|
||
renderRuntimeResult(data.result, data.status);
|
||
if (data.task_id) {
|
||
attachStream(data.task_id);
|
||
}
|
||
}
|
||
|
||
function attachStream(taskId) {
|
||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||
const ws = new WebSocket(`${protocol}//${location.host}/stream/${taskId}`);
|
||
ws.onmessage = (message) => {
|
||
const data = JSON.parse(message.data);
|
||
addEvent(data);
|
||
};
|
||
}
|
||
|
||
async function submitFeedback(payload) {
|
||
if (!currentTaskId) {
|
||
addSystemMessage("Feedback", "Нет активной задачи для оценки.");
|
||
return;
|
||
}
|
||
const response = await fetch("/critic/feedback", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
task_id: currentTaskId,
|
||
session_id: currentSessionId,
|
||
...payload
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
if (data.retry_result) {
|
||
if (Array.isArray(data.retry_result.events)) {
|
||
data.retry_result.events.forEach(addEvent);
|
||
}
|
||
currentTaskId = data.retry_result.task_id || currentTaskId;
|
||
renderRuntimeResult(data.retry_result.result, data.retry_result.status);
|
||
}
|
||
if (data.status !== "ok") {
|
||
addSystemMessage("Feedback", data.message || "Не удалось сохранить feedback.");
|
||
}
|
||
return data;
|
||
}
|
||
|
||
feedbackForm.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const submitter = event.submitter;
|
||
if (submitter && submitter.value === "cancel") {
|
||
feedbackDialog.close();
|
||
return;
|
||
}
|
||
const feedback = feedbackText.value.trim() || "User marked response as incorrect.";
|
||
const data = await submitFeedback({
|
||
feedback_type: feedbackType.value,
|
||
severity: feedbackSeverity.value,
|
||
feedback,
|
||
correction: feedbackCorrection.value.trim(),
|
||
remember: feedbackRemember.checked,
|
||
retry: feedbackRetry.checked,
|
||
assistant_answer: pendingFeedback?.answerText || ""
|
||
});
|
||
if (pendingFeedback?.actions) {
|
||
pendingFeedback.actions.remove();
|
||
}
|
||
pendingFeedback = null;
|
||
feedbackDialog.close();
|
||
if (data?.status === "ok") {
|
||
addSystemMessage("Feedback", "Feedback отправлен.");
|
||
}
|
||
});
|
||
|
||
eventsBtn.addEventListener("click", async () => {
|
||
await loadRecentEvents();
|
||
eventsDialog.showModal();
|
||
});
|
||
refreshEventsBtn.addEventListener("click", async () => {
|
||
await loadRecentEvents();
|
||
});
|
||
closeEventsBtn.addEventListener("click", () => {
|
||
eventsDialog.close();
|
||
});
|
||
sendBtn.addEventListener("click", sendTask);
|
||
</script>
|
||
</body>
|
||
</html>
|