Add structured response feedback
This commit is contained in:
+221
-10
@@ -68,6 +68,14 @@
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.secondary {
|
||||
background: #ffffff;
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
button.danger {
|
||||
background: #b42318;
|
||||
}
|
||||
.messages, .events {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -95,6 +103,48 @@
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.feedback-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
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);
|
||||
}
|
||||
.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; }
|
||||
}
|
||||
@@ -117,6 +167,53 @@
|
||||
<div class="events" id="events"></div>
|
||||
</aside>
|
||||
</div>
|
||||
<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");
|
||||
@@ -130,6 +227,17 @@
|
||||
let activePermissionBubble = null;
|
||||
let activeSecretBubble = null;
|
||||
let activePasswordBubble = null;
|
||||
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");
|
||||
@@ -137,6 +245,7 @@
|
||||
el.innerHTML = `<strong>${title}</strong><div>${body}</div>`;
|
||||
messages.appendChild(el);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return el;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
@@ -147,11 +256,47 @@
|
||||
}
|
||||
|
||||
function addSystemMessage(title, text) {
|
||||
addBubble(title, `<div>${escapeHtml(text)}</div>`);
|
||||
return addBubble(title, `<div>${escapeHtml(text)}</div>`);
|
||||
}
|
||||
|
||||
function addJsonBubble(title, data) {
|
||||
addBubble(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
||||
return addBubble(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) {
|
||||
@@ -180,13 +325,15 @@
|
||||
return;
|
||||
}
|
||||
if (result.message && !result.step_results) {
|
||||
addSystemMessage("Runtime", result.message);
|
||||
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) {
|
||||
addBubble("💻", escapeHtml(toolResult.output));
|
||||
const bubble = addBubble("💻", escapeHtml(toolResult.output));
|
||||
addFeedbackControls(bubble, String(toolResult.output));
|
||||
} else if (toolResult && toolResult.error) {
|
||||
addSystemMessage("❌", toolResult.error);
|
||||
}
|
||||
@@ -194,10 +341,12 @@
|
||||
return;
|
||||
}
|
||||
if (typeof result.output === "string") {
|
||||
addBubble("Runtime", `<pre>${escapeHtml(result.output)}</pre>`);
|
||||
const bubble = addBubble("Runtime", `<pre>${escapeHtml(result.output)}</pre>`);
|
||||
addFeedbackControls(bubble, result.output);
|
||||
return;
|
||||
}
|
||||
addJsonBubble("Runtime", result);
|
||||
const bubble = addJsonBubble("Runtime", result);
|
||||
addFeedbackControls(bubble, JSON.stringify(result));
|
||||
}
|
||||
|
||||
function addEvent(event) {
|
||||
@@ -224,7 +373,8 @@
|
||||
if (directive.type === "respond") {
|
||||
const text = directive.payload?.text || directive.payload?.message || "";
|
||||
if (text) {
|
||||
addBubble("🤖", escapeHtml(text));
|
||||
const bubble = addBubble("🤖", escapeHtml(text));
|
||||
addFeedbackControls(bubble, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,7 +385,8 @@
|
||||
for (const step of execResult.step_results) {
|
||||
const toolResult = step.result?.result || step.result;
|
||||
if (toolResult && toolResult.output) {
|
||||
addBubble("💻", escapeHtml(toolResult.output));
|
||||
const bubble = addBubble("💻", escapeHtml(toolResult.output));
|
||||
addFeedbackControls(bubble, String(toolResult.output));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,10 +609,11 @@
|
||||
}
|
||||
|
||||
async function sendTask() {
|
||||
const taskId = "web-" + Date.now();
|
||||
const body = {
|
||||
input: promptEl.value || "browser task",
|
||||
task_id: "web-" + Date.now(),
|
||||
session_id: "web-session",
|
||||
task_id: taskId,
|
||||
session_id: currentSessionId,
|
||||
context: {}
|
||||
};
|
||||
if (!promptEl.value.trim()) return;
|
||||
@@ -483,6 +635,8 @@
|
||||
lastPermissionRequest = null;
|
||||
lastSecretRequest = null;
|
||||
lastPasswordRequest = null;
|
||||
currentTaskId = data.task_id || taskId;
|
||||
currentSessionId = body.session_id;
|
||||
clearPermissionControls();
|
||||
clearSecretControls();
|
||||
clearPasswordControls();
|
||||
@@ -504,6 +658,63 @@
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
events.innerHTML = "";
|
||||
seenEvents.clear();
|
||||
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 отправлен.");
|
||||
}
|
||||
});
|
||||
|
||||
sendBtn.addEventListener("click", sendTask);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user