feat(feedback): personal AI models, AI run tracking and profiles/options endpoint

- 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
This commit is contained in:
dev
2026-10-07 16:30:27 +03:00
parent a73b1b3f8c
commit a9fb7aaf77
13 changed files with 522 additions and 85 deletions
+81 -15
View File
@@ -17,6 +17,26 @@
.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">
@@ -60,7 +80,7 @@
</div>
<div class="modal-overlay" id="feedbackModal">
<div class="edit-modal" style="max-width:680px">
<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>
@@ -131,7 +151,7 @@
</div>
<div class="modal-overlay" id="fbPromptModal">
<div class="edit-modal" style="max-width:720px">
<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>
@@ -142,24 +162,70 @@
<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="14" maxlength="8000" placeholder="Загрузка…"></textarea>
<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">
<label>Ключи подстановки в шаблон</label>
<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>
<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">