- user_settings allowlist: feedback_models (up to 10 personal model configs) - normalizeFeedbackModels() validation, UI block "Мои модели" in #fbPromptModal - GET /api/ai/profiles/options for tutors (no api_key), used by feedback/shorts - feedbacks columns ai_prompt/ai_model/ai_duration_ms + backup/restore support - "ИИ-запрос" popover on feedback card with status, model, timing and prompt - audit: feedback.update / feedback.ai.run / feedback.ai.error labels
244 lines
15 KiB
HTML
244 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Обратная связь — WhatIDo</title>
|
||
<link rel="stylesheet" href="admin.css">
|
||
<style>
|
||
.feedback-list { display:flex; flex-direction:column; gap:10px; }
|
||
.feedback-item .ai-badge { margin-left:0; }
|
||
.feedback-item .btn-link.sent-on { background:var(--accent); color:#fff; border-color:var(--accent); }
|
||
.shorts-list { display:flex; flex-direction:column; gap:6px; width:100%; max-height:220px; overflow:auto; padding:2px 0; }
|
||
.shorts-list .item { display:flex; gap:8px; align-items:flex-start; }
|
||
.shorts-list .item label { flex:1; font-size:13px; line-height:1.3; color:var(--text); cursor:pointer; }
|
||
.fb-modal-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||
.fb-modal-head h3 { margin:0; }
|
||
.fb-keys { display:flex; flex-direction:column; gap:6px; }
|
||
.fb-keys .fb-key { display:flex; gap:10px; align-items:baseline; font-size:13px; line-height:1.35; }
|
||
.fb-keys code { background:rgba(127,127,127,.12); border:1px solid var(--border); border-radius:6px; padding:1px 6px; font-size:12px; white-space:nowrap; }
|
||
.fb-keys-details { border:1px solid var(--border); border-radius:10px; background:rgba(127,127,127,.05); }
|
||
.fb-keys-details summary { cursor:pointer; list-style:none; display:flex; align-items:center; gap:8px; padding:10px 14px; font-size:.86rem; font-weight:600; color:var(--text); user-select:none; }
|
||
.fb-keys-details summary::-webkit-details-marker { display:none; }
|
||
.fb-keys-details summary::before { content:''; width:7px; height:7px; flex:none; border-right:2px solid var(--muted); border-bottom:2px solid var(--muted); transform:rotate(-45deg); transition:transform .15s; margin-left:2px; }
|
||
.fb-keys-details[open] summary::before { transform:rotate(45deg); }
|
||
.fb-keys-details[open] summary { border-bottom:1px solid var(--border); }
|
||
.fb-keys-details .fb-keys-body { display:flex; flex-direction:column; gap:10px; padding:12px 14px; }
|
||
#fbPromptText { min-height:48vh; resize:vertical; }
|
||
#fbPromptModal { overflow:auto; align-items:flex-start; padding:4vh 16px; }
|
||
#fbPromptModal > .edit-modal { margin:auto; }
|
||
.feedback-item { position:relative; }
|
||
.fb-ai-pop { position:absolute; left:12px; right:12px; top:calc(100% + 6px); z-index:40; display:flex; flex-direction:column; gap:8px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:0 14px 34px rgba(0,0,0,.28); padding:14px 16px; }
|
||
.fb-ai-pop[hidden] { display:none; }
|
||
.fb-ai-row { display:flex; align-items:center; gap:10px; font-size:.85rem; flex-wrap:wrap; }
|
||
.fb-ai-row > span { color:var(--muted); min-width:118px; }
|
||
.fb-ai-row .ai-badge { margin-left:0; }
|
||
.fb-ai-label { font-size:.75rem; font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
|
||
.fb-ai-pop pre { margin:0; max-height:240px; overflow:auto; white-space:pre-wrap; word-break:break-word; font-size:12px; line-height:1.45; background:rgba(127,127,127,.08); border:1px solid var(--border); border-radius:8px; padding:8px 10px; }
|
||
.fb-ai-err { font-size:.83rem; line-height:1.4; color:#ef4444; background:rgba(239,68,68,.08); border:1px solid rgba(239,68,68,.3); border-radius:8px; padding:8px 10px; white-space:pre-wrap; }
|
||
.fb-ai-empty { font-size:.83rem; color:var(--muted); }
|
||
</style>
|
||
</head>
|
||
<body data-page="feedback">
|
||
<div class="layout">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main">
|
||
<div class="page-head">
|
||
<h2>Обратная связь</h2>
|
||
<p class="page-sub">Отзывы тьютора по итогам занятий</p>
|
||
</div>
|
||
|
||
<div class="filters">
|
||
<div><label>Поиск</label><input type="text" id="fbSearch" class="search-input" placeholder="Поиск по ФИО, теме, тексту…"></div>
|
||
<div><label>Группа</label><select id="fbGroupFilter"><option value="">Все группы</option></select></div>
|
||
<div><label>Статус</label><select id="fbStatusFilter">
|
||
<option value="">Все статусы</option>
|
||
<option value="sent">Отправлено</option>
|
||
<option value="none">Без ИИ</option>
|
||
<option value="pending">Ожидает</option>
|
||
<option value="processing">В обработке</option>
|
||
<option value="done">Готово</option>
|
||
<option value="error">Ошибка</option>
|
||
<option value="skipped">Пропущен</option>
|
||
</select></div>
|
||
<div><label>С</label><input type="date" id="fbDateFrom"></div>
|
||
<div><label>По</label><input type="date" id="fbDateTo"></div>
|
||
<div><label> </label>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="fbDeletedToggle">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text">Удалённые</span>
|
||
</label>
|
||
</div>
|
||
<div><label> </label><button type="button" class="btn-primary" id="addFeedbackBtn"><span data-lucide="plus"></span> Добавить отзыв</button></div>
|
||
</div>
|
||
|
||
<div class="hint" id="feedbackState" style="margin-bottom:12px"></div>
|
||
<div class="feedback-list" id="feedbackList"><div class="empty">Загрузка…</div></div>
|
||
<div class="pager" id="pager"></div>
|
||
</main>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="feedbackModal">
|
||
<div class="edit-modal" style="max-width:960px;width:96%;max-height:92dvh;overflow-y:auto">
|
||
<div class="fb-modal-head">
|
||
<h3 id="fbModalTitle">Добавить отзыв</h3>
|
||
<button type="button" class="icon-btn" id="fbPromptBtn" title="Настройки ИИ-промпта"><i data-lucide="settings"></i></button>
|
||
</div>
|
||
<form id="fbForm" class="settings-stack" autocomplete="off">
|
||
<input type="hidden" id="fbId">
|
||
<div class="settings-stack-grid">
|
||
<div class="settings-field">
|
||
<label>Дата занятия <span style="color:#ef4444">*</span></label>
|
||
<input type="date" id="fbDate" class="settings-input" required>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Группа <span style="color:#ef4444">*</span></label>
|
||
<select id="fbGroup" class="settings-input" required></select>
|
||
</div>
|
||
</div>
|
||
<div class="settings-stack-grid">
|
||
<div class="settings-field">
|
||
<label>Фамилия/Имя резидента <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="fbResident" class="settings-input" required maxlength="200" placeholder="Иванов Иван" list="fbResidentList" autocomplete="off">
|
||
<datalist id="fbResidentList"></datalist>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Тема занятия</label>
|
||
<input type="text" id="fbTopic" class="settings-input" maxlength="500" placeholder="Тема занятия" list="fbTopicList" autocomplete="off">
|
||
<datalist id="fbTopicList"></datalist>
|
||
</div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Текст отзыва</label>
|
||
<textarea id="fbText" class="settings-input" rows="5" maxlength="5000" placeholder="Текст отзыва (может быть сгенерирован ИИ)"></textarea>
|
||
<div class="hint"><span id="fbTextHint">0 / 5000</span></div>
|
||
</div>
|
||
<div class="settings-stack-grid" style="align-items:start">
|
||
<label class="toggle" style="align-items:center">
|
||
<input type="checkbox" id="fbAiCheck" checked>
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Сгенерировать отзыв ИИ</b><small>Будет отправлено в очередь</small></span>
|
||
</label>
|
||
<div class="settings-field">
|
||
<label>Модель ИИ</label>
|
||
<select id="fbModel" class="settings-input"></select>
|
||
</div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label class="toggle" style="align-items:center">
|
||
<input type="checkbox" id="fbPastCheck">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text">Добавить в промпт прошлый отзыв</span>
|
||
</label>
|
||
</div>
|
||
<div class="settings-field" id="fbPastReviewWrap" style="display:none">
|
||
<label>Прошлый отзыв</label>
|
||
<textarea id="fbPastReview" class="settings-input" rows="3" maxlength="2000"></textarea>
|
||
</div>
|
||
<div class="settings-field" id="fbShortsWrap" style="display:none">
|
||
<label>Выберите короткие сообщения для обратной связи (отмечены все)</label>
|
||
<div class="shorts-list" id="fbShortsList"></div>
|
||
<div class="hint">Эти сообщения будут переданы в промпт как контекст</div>
|
||
</div>
|
||
<div class="ai-status" id="fbAiStatus" hidden></div>
|
||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||
<button type="button" class="btn-primary ghost" id="fbCancel">Отмена</button>
|
||
<button type="submit" class="btn-primary" id="fbSubmit">Отправить в очередь</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="fbPromptModal">
|
||
<div class="edit-modal" style="max-width:900px">
|
||
<div class="fb-modal-head">
|
||
<h3>Настройки ИИ-промпта</h3>
|
||
<button type="button" class="icon-btn" id="fbPromptClose" title="Закрыть"><i data-lucide="x"></i></button>
|
||
</div>
|
||
<div class="settings-stack">
|
||
<div class="settings-field">
|
||
<label>Модель по умолчанию</label>
|
||
<select id="fbDefaultModel" class="settings-input"></select>
|
||
<div class="hint">Подставляется в поле «Модель ИИ» при добавлении отзыва. Пусто — активная модель системы</div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<details class="fb-keys-details" id="fbModelsDetails">
|
||
<summary>Мои модели<span id="fbModelsCount" class="hint"></span></summary>
|
||
<div class="fb-keys-body">
|
||
<div id="fbModelsList"></div>
|
||
<div class="hint">Свои подключения к ИИ. Личные модели идут в списках первыми, системные остаются запасными</div>
|
||
<div id="fbModelEdit" class="settings-stack" hidden>
|
||
<div class="settings-stack-grid">
|
||
<div class="settings-field">
|
||
<label>Название</label>
|
||
<input id="fbMName" class="settings-input" maxlength="64" placeholder="Моя модель">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Модель</label>
|
||
<input id="fbMModel" class="settings-input" maxlength="100" placeholder="gpt-oss:120b">
|
||
</div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Base URL</label>
|
||
<input id="fbMUrl" class="settings-input" maxlength="300" placeholder="http://host:11434/v1">
|
||
</div>
|
||
<div class="settings-stack-grid">
|
||
<div class="settings-field">
|
||
<label>API-ключ</label>
|
||
<input id="fbMKey" class="settings-input" maxlength="256" placeholder="необязательно">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>max_tokens</label>
|
||
<input id="fbMMaxTok" class="settings-input" type="number" min="16" max="131072" placeholder="авто">
|
||
</div>
|
||
</div>
|
||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||
<button type="button" class="btn-primary ghost" id="fbMCancel">Отмена</button>
|
||
<button type="button" class="btn-primary" id="fbMSave">Готово</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button type="button" class="btn-primary ghost" id="fbModelAdd">+ Добавить модель</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Шаблон промпта</label>
|
||
<textarea id="fbPromptText" class="settings-input" rows="24" maxlength="8000" placeholder="Загрузка…"></textarea>
|
||
<div class="hint"><span id="fbPromptCount">0 / 8000</span> · Пусто — используется системный шаблон</div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<details class="fb-keys-details" id="fbPromptKeys">
|
||
<summary>Ключи подстановки в шаблон</summary>
|
||
<div class="fb-keys-body">
|
||
<div class="fb-keys">
|
||
<div class="fb-key"><code>{group}</code><span>название группы</span></div>
|
||
<div class="fb-key"><code>{date}</code><span>дата занятия (ГГГГ-ММ-ДД)</span></div>
|
||
<div class="fb-key"><code>{resident}</code><span>фамилия и имя резидента</span></div>
|
||
<div class="fb-key"><code>{topic}</code><span>тема занятия</span></div>
|
||
<div class="fb-key"><code>{past_review}</code><span>прошлый отзыв, или «нет», если его нет</span></div>
|
||
<div class="fb-key"><code>{shorts}</code><span>выбранные короткие сообщения: «- дата – тема – текст»</span></div>
|
||
<div class="fb-key"><code>{context}</code><span>стандартный блок контекста занятия целиком: группа, дата, резидент, тема, прошлый отзыв, короткие сообщения</span></div>
|
||
<div class="fb-key"><code>{text}</code><span>черновик текста отзыва из формы</span></div>
|
||
</div>
|
||
<div class="hint">Если шаблон содержит хотя бы один ключ — данные подставляются по ним, и автоматический блок «КОНТЕКСТ» в конец промпта не добавляется. Без ключей блок «КОНТЕКСТ» добавляется в конец автоматически.</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<div class="ai-status" id="fbPromptStatus" hidden></div>
|
||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||
<button type="button" class="btn-primary ghost" id="fbPromptReset">Сбросить</button>
|
||
<button type="button" class="btn-primary" id="fbPromptSave">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="vendor/lucide.min.js"></script>
|
||
<script src="js/datetime.js"></script>
|
||
<script src="admin.js"></script>
|
||
<script src="js/feedback.js"></script>
|
||
</body>
|
||
</html> |