Add structured response feedback

This commit is contained in:
2026-05-10 23:57:50 +08:00
parent 2ba18e0b35
commit 77c2e37b95
4 changed files with 367 additions and 35 deletions
+221 -10
View File
@@ -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>