From 23213f2d75a06b0b1868304d3569fd37dffbd57d Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Mon, 28 Sep 2026 16:32:01 +0300 Subject: [PATCH] Admin grades: comment autocomplete by substring, larger grade dialog --- .../ysu/lms/web/admin/AdminGroupController.kt | 10 +++ web/src/main/resources/static/css/app.css | 15 +++- .../main/resources/static/js/admin-group.js | 75 +++++++++++++++++++ .../main/resources/templates/admin/group.html | 2 +- 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt index 97c524b..3b71415 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.multipart.MultipartFile import org.springframework.web.servlet.mvc.support.RedirectAttributes 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.publishGrades import ru.ysu.lms.web.api.setGrade +import ru.ysu.lms.web.api.suggestComments import tools.jackson.databind.ObjectMapper import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -233,6 +235,14 @@ class AdminGroupController( "redirect:/admin/groups/$id" } + /** Подсказки ранее введённых комментариев: поиск по подстроке, частые — выше. */ + @GetMapping("/comment-templates") + @ResponseBody + fun commentTemplates( + @RequestParam(required = false, defaultValue = "") q: String, + session: HttpSession, + ): List> = api.suggestComments(token(session), q).map { mapOf("text" to it.text, "tone" to it.tone) } + @GetMapping("/groups/{id}/submissions.zip") fun downloadGroupSubmissions( @PathVariable id: String, diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index cd58f24..02ce21c 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -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-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-pos { background: #16803c; color: #fff; } diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js index c663b51..7cd68e2 100644 --- a/web/src/main/resources/static/js/admin-group.js +++ b/web/src/main/resources/static/js/admin-group.js @@ -54,6 +54,81 @@ function csRow(text, tone) { row.appendChild(sel); row.appendChild(rm); 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() { diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html index 7f7e07c..bab5bec 100644 --- a/web/src/main/resources/templates/admin/group.html +++ b/web/src/main/resources/templates/admin/group.html @@ -129,7 +129,7 @@ - +