Admin grades: comment autocomplete by substring, larger grade dialog
This commit is contained in:
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PathVariable
|
|||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes
|
||||||
import ru.ysu.lms.markdown.MarkdownRenderer
|
import ru.ysu.lms.markdown.MarkdownRenderer
|
||||||
@@ -35,6 +36,7 @@ import ru.ysu.lms.web.api.listGroups
|
|||||||
import ru.ysu.lms.web.api.listLabs
|
import ru.ysu.lms.web.api.listLabs
|
||||||
import ru.ysu.lms.web.api.publishGrades
|
import ru.ysu.lms.web.api.publishGrades
|
||||||
import ru.ysu.lms.web.api.setGrade
|
import ru.ysu.lms.web.api.setGrade
|
||||||
|
import ru.ysu.lms.web.api.suggestComments
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -233,6 +235,14 @@ class AdminGroupController(
|
|||||||
"redirect:/admin/groups/$id"
|
"redirect:/admin/groups/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Подсказки ранее введённых комментариев: поиск по подстроке, частые — выше. */
|
||||||
|
@GetMapping("/comment-templates")
|
||||||
|
@ResponseBody
|
||||||
|
fun commentTemplates(
|
||||||
|
@RequestParam(required = false, defaultValue = "") q: String,
|
||||||
|
session: HttpSession,
|
||||||
|
): List<Map<String, String>> = api.suggestComments(token(session), q).map { mapOf("text" to it.text, "tone" to it.tone) }
|
||||||
|
|
||||||
@GetMapping("/groups/{id}/submissions.zip")
|
@GetMapping("/groups/{id}/submissions.zip")
|
||||||
fun downloadGroupSubmissions(
|
fun downloadGroupSubmissions(
|
||||||
@PathVariable id: String,
|
@PathVariable id: String,
|
||||||
|
|||||||
@@ -291,7 +291,20 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
|
|||||||
.chip-neu { background: #5d6570; color: #fff; }
|
.chip-neu { background: #5d6570; color: #fff; }
|
||||||
.chip-neg { background: #b3283b; color: #fff; }
|
.chip-neg { background: #b3283b; color: #fff; }
|
||||||
/* Слоты комментариев в диалоге */
|
/* Слоты комментариев в диалоге */
|
||||||
.cs-row { display: flex; gap: 6px; align-items: center; }
|
.cs-row { display: flex; gap: 6px; align-items: center; position: relative; }
|
||||||
|
.cs-row .cs-text { flex: 1 1 auto; min-width: 0; }
|
||||||
|
.ac-list {
|
||||||
|
position: absolute; left: 0; top: 100%; margin-top: 2px; z-index: 20;
|
||||||
|
min-width: 100%; max-height: 14rem; overflow-y: auto;
|
||||||
|
background: var(--c-surface); border: 1px solid var(--c-border);
|
||||||
|
border-radius: 0.4rem; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.ac-item { padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.875rem; }
|
||||||
|
.ac-item:hover, .ac-item.active { background: var(--c-bg); }
|
||||||
|
.ac-empty { padding: 0.4rem 0.6rem; font-size: 0.875rem; color: var(--c-text-muted); }
|
||||||
|
/* Увеличенный диалог (оценка + комментарии) */
|
||||||
|
dialog.modal.modal-lg { width: min(56rem, calc(100vw - 2rem)); }
|
||||||
|
dialog.modal.modal-lg .modal-body { max-height: 78vh; }
|
||||||
|
|
||||||
.chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; line-height: 1.5; }
|
.chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; line-height: 1.5; }
|
||||||
.chip-pos { background: #16803c; color: #fff; }
|
.chip-pos { background: #16803c; color: #fff; }
|
||||||
|
|||||||
@@ -54,6 +54,81 @@ function csRow(text, tone) {
|
|||||||
row.appendChild(sel);
|
row.appendChild(sel);
|
||||||
row.appendChild(rm);
|
row.appendChild(rm);
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
|
|
||||||
|
attachCommentAutocomplete(inp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Автодополнение комментариев ---------- *
|
||||||
|
* При фокусе/вводе запрашивает ранее введённые комментарии (подстрока, без регистра),
|
||||||
|
* отсортированные по частоте; выбор подставляет текст и «тон». */
|
||||||
|
|
||||||
|
var AC_TIMER = null;
|
||||||
|
|
||||||
|
function attachCommentAutocomplete(input) {
|
||||||
|
var list = document.createElement('div');
|
||||||
|
list.className = 'ac-list';
|
||||||
|
list.hidden = true;
|
||||||
|
input.parentNode.appendChild(list);
|
||||||
|
|
||||||
|
var active = -1;
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
list.hidden = true;
|
||||||
|
active = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(items) {
|
||||||
|
if (document.activeElement !== input) return;
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = '';
|
||||||
|
items.forEach(function (item) {
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'ac-item';
|
||||||
|
row.textContent = item.text;
|
||||||
|
row.addEventListener('mousedown', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
input.value = item.text;
|
||||||
|
var tone = input.parentNode.querySelector('.cs-tone');
|
||||||
|
if (tone && item.tone) tone.value = item.tone;
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
list.hidden = false;
|
||||||
|
active = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
clearTimeout(AC_TIMER);
|
||||||
|
AC_TIMER = setTimeout(function () {
|
||||||
|
fetch('/admin/comment-templates?q=' + encodeURIComponent(input.value.trim()))
|
||||||
|
.then(function (r) { return r.ok ? r.json() : []; })
|
||||||
|
.then(render)
|
||||||
|
.catch(hide);
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('focus', load);
|
||||||
|
input.addEventListener('input', load);
|
||||||
|
input.addEventListener('blur', function () { setTimeout(hide, 150); });
|
||||||
|
input.addEventListener('keydown', function (event) {
|
||||||
|
if (list.hidden) return;
|
||||||
|
var items = list.querySelectorAll('.ac-item');
|
||||||
|
if (!items.length) return;
|
||||||
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
||||||
|
items.forEach(function (el, i) { el.classList.toggle('active', i === active); });
|
||||||
|
} else if (event.key === 'Enter' && active >= 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
items[active].dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function csSerialize() {
|
function csSerialize() {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Диалог: оценка + комментарии -->
|
<!-- Диалог: оценка + комментарии -->
|
||||||
<dialog id="modal-grade" class="modal">
|
<dialog id="modal-grade" class="modal modal-lg">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
|
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
|
||||||
<button class="modal-close" type="button" onclick="closeModal('modal-grade')">✕</button>
|
<button class="modal-close" type="button" onclick="closeModal('modal-grade')">✕</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user