feat(flashcards): ИИ-генерация карточек по теме/тексту с предпросмотром в текущую колоду

This commit is contained in:
Maxim Dolgolyov
2026-06-12 23:06:08 +03:00
parent 21cea72874
commit 9dd3522869
3 changed files with 69 additions and 7 deletions
@@ -588,12 +588,15 @@ async function flashcardsFromText(req, res) {
if (!providersOrdered().length) return res.status(503).json({ error: 'LLM не настроена' });
const text = String((req.body && req.body.text) || '').trim().slice(0, 6000);
const title = String((req.body && req.body.title) || 'Карточки').trim().slice(0, 80) || 'Карточки';
if (text.length < 20) return res.status(400).json({ error: 'Слишком мало текста' });
const sys = 'Ты составляешь учебные флешкарты по тексту. Верни СТРОГО JSON-массив из 5–6 объектов ' +
'вида {"front":"...","back":"..."} без markdown и пояснений. front — короткий вопрос, back — краткий ответ (1–2 предложения). ' +
'По-русски. Формулы в LaTeX между $...$. Никакого текста вне JSON.';
let count = Number(req.body && req.body.count);
count = Number.isFinite(count) ? Math.max(3, Math.min(10, Math.round(count))) : 6;
if (text.length < 3) return res.status(400).json({ error: 'Введите тему или текст' });
const sys = 'Ты составляешь учебные флешкарты. Если на вход дан учебный текст или параграф — делай карточки СТРОГО по нему. ' +
'Если дана короткая тема (несколько слов) — раскрой её сам по школьной программе. ' +
'Верни СТРОГО JSON-массив из ' + count + ' объектов вида {"front":"...","back":"..."} без markdown и пояснений. ' +
'front — короткий вопрос, back — краткий ответ (1–2 предложения). По-русски. Формулы в LaTeX между $...$. Никакого текста вне JSON.';
let rr;
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 1400); }
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 1600); }
catch (e) { return res.status(502).json({ error: 'Не удалось обратиться к ИИ' }); }
const raw = rr && rr.text;
let cards = [];
@@ -609,7 +612,7 @@ async function flashcardsFromText(req, res) {
const arr = JSON.parse(s);
if (Array.isArray(arr)) {
cards = arr.filter(c => c && c.front && c.back)
.slice(0, 8)
.slice(0, count + 2)
.map(c => ({ front: String(c.front).slice(0, 500), back: String(c.back).slice(0, 1000) }));
}
} catch (e) { /* модель вернула не-JSON */ }
+59
View File
@@ -398,6 +398,7 @@
Колоды
</button>
<h1 class="fc-title" id="cards-deck-title">Название колоды</h1>
<button class="fc-btn fc-btn-ghost" id="cards-ai-btn" onclick="openAiGenModal()">Сгенерировать ИИ</button>
<button class="fc-btn fc-btn-ghost" onclick="openBulkModal()">Добавить список</button>
<button class="fc-btn fc-btn-primary" id="cards-study-btn" onclick="startStudy()">Начать изучение</button>
</div>
@@ -536,6 +537,29 @@
</div>
</div>
<!-- ── AI generate Modal ── -->
<div class="fc-modal" id="modal-aigen">
<div class="fc-modal-bg" onclick="closeModal('modal-aigen')"></div>
<div class="fc-modal-box" style="max-width:560px">
<div class="fc-modal-title">Сгенерировать карточки ИИ</div>
<div class="fc-modal-field">
<div class="fc-modal-label">Тема или текст</div>
<textarea class="fc-modal-input" id="aigen-text" rows="6" style="resize:vertical"
placeholder="Например: Теорема Пифагора&#10;— или вставьте параграф / конспект, по которому сделать карточки"></textarea>
</div>
<div class="fc-modal-field" style="display:flex;align-items:center;gap:10px">
<span class="fc-modal-label" style="margin:0">Сколько карточек</span>
<select class="fc-modal-input" id="aigen-count" style="width:auto;padding:8px 12px">
<option>4</option><option selected>6</option><option>8</option><option>10</option>
</select>
</div>
<div class="fc-modal-actions">
<button class="fc-btn fc-btn-ghost" onclick="closeModal('modal-aigen')">Отмена</button>
<button class="fc-btn fc-btn-primary" id="aigen-btn" onclick="runAiGen()">Сгенерировать</button>
</div>
</div>
</div>
<!-- ── Formula (KaTeX) Modal ── -->
<div class="fc-modal" id="modal-formula">
<div class="fc-modal-bg" onclick="closeModal('modal-formula')"></div>
@@ -1204,6 +1228,41 @@ async function saveBulk() {
closeModal('modal-bulk');
}
/* ════ Генерация карточек ИИ (тема/текст → предпросмотр bulk → текущая колода) ════
Переиспользует экран предпросмотра bulk-импорта: ИИ заполняет _bulkCards,
пользователь правит и сохраняет в текущую колоду через saveBulk(). */
function openAiGenModal() {
if (!_curDeck) { LS.toast('Сначала откройте колоду', 'error'); return; }
document.getElementById('aigen-text').value = '';
document.getElementById('modal-aigen').classList.add('open');
setTimeout(() => { try { document.getElementById('aigen-text').focus(); } catch (e) {} }, 50);
}
async function runAiGen() {
const text = document.getElementById('aigen-text').value.trim();
if (text.length < 3) { LS.toast('Введите тему или текст'); return; }
const count = Number(document.getElementById('aigen-count').value) || 6;
const btn = document.getElementById('aigen-btn');
btn.disabled = true; btn.textContent = 'Генерирую…';
try {
const r = await LS.assistantFlashcards(text, (_curDeck && _curDeck.title) || 'Карточки', count);
const cards = (r && r.cards) || [];
if (!cards.length) throw new Error('ИИ не вернул карточек');
_bulkCards = cards.map(c => ({ front: c.front || '', back: c.back || '', front_image: '', back_image: '' }));
closeModal('modal-aigen');
// открыть bulk-модалку сразу на шаге предпросмотра
document.getElementById('bulk-text').value = '';
document.getElementById('bulk-step-text').style.display = 'none';
document.getElementById('bulk-step-preview').style.display = '';
document.getElementById('modal-bulk').classList.add('open');
renderBulkPreview();
} catch (e) {
LS.toast(e && e.message ? ('ИИ: ' + e.message) : 'Не удалось сгенерировать', 'error');
} finally {
btn.disabled = false; btn.textContent = 'Сгенерировать';
}
}
/* ════ Formula insert (KaTeX) ════
Палитра символов перенесена из редактора теории (lesson-editor.html).
Текст карточки свободный — вставляем \( … \) (в строке) или \[ … \] (блоком)
+1 -1
View File
@@ -1264,7 +1264,7 @@ async function assistantSeen(ruleId) { return req('POST', '/assistant/seen', {
async function assistantDismiss(rid) { return req('POST', '/assistant/dismiss', { ruleId: rid }); }
async function assistantSettings(d) { return req('PATCH', '/assistant/settings', d); }
async function assistantAsk(q, context, history, mode) { return req('POST', '/assistant/ask', { q, context: context || undefined, history: history || undefined, mode: mode || undefined }); }
async function assistantFlashcards(text, title) { return req('POST', '/assistant/flashcards', { text, title }); }
async function assistantFlashcards(text, title, count) { return req('POST', '/assistant/flashcards', { text, title, count }); }
async function assistantFeedback(rating, q) { return req('POST', '/assistant/feedback', { rating, q: q || undefined }); }
async function assistantMemory() { return req('GET', '/assistant/memory'); }
async function assistantMemoryClear(id) { return req('DELETE', '/assistant/memory' + (id ? '/' + id : '')); }