feat: per-user feedback AI prompt template with placeholder keys
This commit is contained in:
+49
-1
@@ -12,6 +12,11 @@
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="feedback">
|
||||
@@ -56,7 +61,10 @@
|
||||
|
||||
<div class="modal-overlay" id="feedbackModal">
|
||||
<div class="edit-modal" style="max-width:680px">
|
||||
<h3 id="fbModalTitle">Добавить отзыв</h3>
|
||||
<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">
|
||||
@@ -122,6 +130,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="fbPromptModal">
|
||||
<div class="edit-modal" style="max-width:720px">
|
||||
<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">
|
||||
<label>Шаблон промпта</label>
|
||||
<textarea id="fbPromptText" class="settings-input" rows="14" 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>
|
||||
</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>
|
||||
|
||||
+81
-1
@@ -1,7 +1,7 @@
|
||||
(async function() {
|
||||
const API = location.origin;
|
||||
const PAGE_SIZE = 20;
|
||||
let page = 1, total = 0, searchTimer, groupsLoaded, feedbacks = [], shortsListCache = null, shortsFetchTimer = null, lastFbAutoText = '';
|
||||
let page = 1, total = 0, searchTimer, groupsLoaded, feedbacks = [], shortsListCache = null, shortsFetchTimer = null, lastFbAutoText = '', userSettings = { settings: {}, defaults: {} }, aiActive = 'native';
|
||||
const topicReportCache = new Map();
|
||||
|
||||
function hdr() { const t = sessionStorage.getItem('authToken'); return t ? { 'X-Auth-Token': t } : {}; }
|
||||
@@ -68,6 +68,69 @@
|
||||
function closeModal() {
|
||||
const m = document.getElementById('feedbackModal');
|
||||
if (m) m.classList.remove('open');
|
||||
closePromptModal();
|
||||
}
|
||||
|
||||
function closePromptModal() {
|
||||
const m = document.getElementById('fbPromptModal');
|
||||
if (m) m.classList.remove('open');
|
||||
}
|
||||
|
||||
function updatePromptCount() {
|
||||
const el = document.getElementById('fbPromptCount');
|
||||
const ta = document.getElementById('fbPromptText');
|
||||
if (el && ta) el.textContent = `${ta.value.length} / 8000`;
|
||||
}
|
||||
|
||||
async function loadUserSettings() {
|
||||
try {
|
||||
const r = await fetch(`${API}/api/user-settings`, { headers: hdr() });
|
||||
if (!r.ok) return;
|
||||
const j = await r.json();
|
||||
userSettings.settings = j.settings || {};
|
||||
userSettings.defaults = j.defaults || {};
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function openPromptModal() {
|
||||
const src = document.getElementById('fbModel');
|
||||
const dst = document.getElementById('fbDefaultModel');
|
||||
dst.innerHTML = '<option value="">Активная модель по умолчанию</option>' + src.innerHTML;
|
||||
dst.value = userSettings.settings.feedback_profile || '';
|
||||
const ta = document.getElementById('fbPromptText');
|
||||
ta.value = userSettings.settings.feedback_prompt || '';
|
||||
ta.placeholder = userSettings.defaults.feedback_prompt || 'Системный шаблон';
|
||||
document.getElementById('fbPromptStatus').hidden = true;
|
||||
updatePromptCount();
|
||||
document.getElementById('fbPromptModal').classList.add('open');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
async function savePromptSettings() {
|
||||
const st = document.getElementById('fbPromptStatus');
|
||||
const body = {
|
||||
settings: {
|
||||
feedback_prompt: document.getElementById('fbPromptText').value.trim(),
|
||||
feedback_profile: document.getElementById('fbDefaultModel').value || '',
|
||||
}
|
||||
};
|
||||
try {
|
||||
const r = await fetch(`${API}/api/user-settings`, { method: 'PUT', headers: {...hdr(), 'Content-Type': 'application/json'}, body: JSON.stringify(body) });
|
||||
const j = await r.json().catch(() => ({}));
|
||||
if (!r.ok) {
|
||||
st.hidden = false;
|
||||
st.innerHTML = `<span class="ai-badge error">${escapeHtml(j.error || 'Не удалось сохранить')}</span>`;
|
||||
return;
|
||||
}
|
||||
userSettings.settings = j.settings || {};
|
||||
userSettings.defaults = j.defaults || userSettings.defaults;
|
||||
st.hidden = false;
|
||||
st.innerHTML = '<span class="ai-badge done">Сохранено</span>';
|
||||
setTimeout(closePromptModal, 700);
|
||||
} catch (e) {
|
||||
st.hidden = false;
|
||||
st.innerHTML = '<span class="ai-badge error">Не удалось сохранить</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function todayIso() {
|
||||
@@ -170,6 +233,7 @@
|
||||
const nativeOpt = document.createElement('option'); nativeOpt.value = 'native'; nativeOpt.textContent = 'Нативная (llama.cpp)'; sel.appendChild(nativeOpt);
|
||||
(d.profiles||[]).forEach(p => { const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; sel.appendChild(o); });
|
||||
if (d.active) sel.value = d.active;
|
||||
aiActive = d.active || 'native';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -226,6 +290,10 @@
|
||||
document.getElementById('fbTextHint').textContent = '0 / 5000';
|
||||
document.getElementById('fbAiCheck').checked = true;
|
||||
document.getElementById('fbAiStatus').hidden = true;
|
||||
const ms = document.getElementById('fbModel');
|
||||
const pref = userSettings.settings.feedback_profile;
|
||||
if (pref && [...ms.options].some(o => o.value === pref)) ms.value = pref;
|
||||
else if (aiActive && [...ms.options].some(o => o.value === aiActive)) ms.value = aiActive;
|
||||
document.getElementById('fbModalTitle').textContent = 'Добавить отзыв';
|
||||
document.getElementById('fbSubmit').textContent = 'Отправить в очередь';
|
||||
}
|
||||
@@ -309,6 +377,7 @@
|
||||
await checkAuth().catch(()=>{});
|
||||
buildSidebar('feedback');
|
||||
await loadGroups();
|
||||
await loadUserSettings();
|
||||
await loadAiProfiles();
|
||||
await loadResidentNames();
|
||||
loadTopics();
|
||||
@@ -316,6 +385,17 @@
|
||||
connectFeedbackLive();
|
||||
document.getElementById('addFeedbackBtn').addEventListener('click', () => openModal({}));
|
||||
document.getElementById('fbCancel').addEventListener('click', closeModal);
|
||||
document.getElementById('fbPromptBtn').addEventListener('click', openPromptModal);
|
||||
document.getElementById('fbPromptClose').addEventListener('click', closePromptModal);
|
||||
document.getElementById('fbPromptModal').addEventListener('click', e => { if (e.target.id === 'fbPromptModal') closePromptModal(); });
|
||||
document.getElementById('fbPromptText').addEventListener('input', updatePromptCount);
|
||||
document.getElementById('fbPromptReset').addEventListener('click', () => {
|
||||
document.getElementById('fbPromptText').value = '';
|
||||
document.getElementById('fbDefaultModel').value = '';
|
||||
document.getElementById('fbPromptStatus').hidden = true;
|
||||
updatePromptCount();
|
||||
});
|
||||
document.getElementById('fbPromptSave').addEventListener('click', savePromptSettings);
|
||||
document.getElementById('feedbackModal').addEventListener('click', e => { if (e.target.id === 'feedbackModal') closeModal(); });
|
||||
document.getElementById('fbForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user