feat(assistant): чёткий ответ при лимите ИИ (память не теряется), напоминание о памяти, отдельный раздел в админке
- Баг «не помнит»: на самом деле free-лимит Gemini (429). callLLM теперь возвращает ошибку; при 429 показываем «много запросов, подожди минутку — память не потеряется» и НЕ ломаем историю (убираем неудачный вопрос); при сбое — «не получилось, попробуй позже». Раньше показывалось «не нашёл ответ». - В окне «Спроси» — пояснение, сколько помнит Квантик (≈6 реплик, рабочая память). - Окна красивее: шире, аватар Квантика в шапке, мягкая анимация. - Управление помощником вынесено в отдельный раздел админки «Помощник Квантик» (системный вкл/выкл + модель/ключ/тест/RAG/кнопки экзамена/статистика/качество); из раздела «Игры» конфиг убран. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -269,9 +269,10 @@ function bumpUsage(field) {
|
||||
}
|
||||
|
||||
/* Низкоуровневый вызов OpenAI-совместимого chat/completions. */
|
||||
/* Возвращает { text, error } — error: 'off'|'rate_limit'|'http'|'timeout'|'network'|'empty'|null. */
|
||||
async function callLLM(messages, maxTokens, override) {
|
||||
const cfg = override || llmConfig();
|
||||
if (typeof fetch !== 'function' || !cfg.on) return null;
|
||||
if (typeof fetch !== 'function' || !cfg.on) return { text: null, error: 'off' };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 15000);
|
||||
try {
|
||||
@@ -281,11 +282,11 @@ async function callLLM(messages, maxTokens, override) {
|
||||
body: JSON.stringify({ model: cfg.model, temperature: 0.3, max_tokens: maxTokens || 320, messages }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!r.ok) return null;
|
||||
if (!r.ok) return { text: null, error: r.status === 429 ? 'rate_limit' : 'http', status: r.status };
|
||||
const data = await r.json();
|
||||
const text = data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
|
||||
return text ? String(text).trim() : null;
|
||||
} catch (e) { return null; } finally { clearTimeout(timer); }
|
||||
return { text: text ? String(text).trim() : null, error: text ? null : 'empty' };
|
||||
} catch (e) { return { text: null, error: e.name === 'AbortError' ? 'timeout' : 'network' }; } finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
/* Тест-пинг для админки: подробный статус (status/ошибка/пример ответа). */
|
||||
@@ -383,15 +384,23 @@ async function ask(req, res) {
|
||||
let context = pageCtx;
|
||||
if (rag.text) context = (context ? context + '\n\n' : '') + 'Из учебников:\n' + rag.text;
|
||||
|
||||
let answer = null;
|
||||
try { answer = await askModel(q, hits, context, history, req.user && req.user.role, mode); } catch (e) { answer = null; }
|
||||
let r = { text: null, error: 'network' };
|
||||
try { r = await askModel(q, hits, context, history, req.user && req.user.role, mode); } catch (e) { r = { text: null, error: 'network' }; }
|
||||
const answer = r && r.text;
|
||||
|
||||
if (answer) {
|
||||
bumpUsage('model_calls');
|
||||
if (cacheable) { try { db.prepare("INSERT OR REPLACE INTO assistant_cache (qhash, answer, created_at) VALUES (?, ?, datetime('now'))").run(qhash, answer); } catch (e) {} }
|
||||
} else { bumpUsage('faq'); }
|
||||
|
||||
res.json({ source: answer ? 'model' : 'faq', answer: answer || null, answers: faqJson, sources: answer ? rag.sources : [] });
|
||||
return res.json({ source: 'model', answer, answers: faqJson, sources: rag.sources });
|
||||
}
|
||||
bumpUsage('faq');
|
||||
if (r && r.error === 'rate_limit') {
|
||||
return res.json({ source: 'limit', answer: 'Сейчас слишком много запросов к ИИ за короткое время — подожди минутку и спроси снова. Память диалога не потеряется.', answers: faqJson, sources: [] });
|
||||
}
|
||||
if (r && (r.error === 'timeout' || r.error === 'network' || r.error === 'http')) {
|
||||
return res.json({ source: 'error', answer: 'Не получилось обратиться к ИИ. Попробуй ещё раз чуть позже.', answers: faqJson, sources: [] });
|
||||
}
|
||||
res.json({ source: 'faq', answer: null, answers: faqJson, sources: [] });
|
||||
}
|
||||
|
||||
/* ── POST /api/assistant/feedback { rating, q? } ── лайк/дизлайк ответа ── */
|
||||
@@ -414,7 +423,8 @@ async function flashcardsFromText(req, res) {
|
||||
const sys = 'Ты составляешь учебные флешкарты по тексту. Верни СТРОГО JSON-массив из 5–6 объектов ' +
|
||||
'вида {"front":"...","back":"..."} без markdown и пояснений. front — короткий вопрос, back — краткий ответ (1–2 предложения). ' +
|
||||
'По-русски. Формулы в LaTeX между $...$. Никакого текста вне JSON.';
|
||||
const raw = await callLLM([{ role: 'system', content: sys }, { role: 'user', content: text }], 1400);
|
||||
const rr = await callLLM([{ role: 'system', content: sys }, { role: 'user', content: text }], 1400);
|
||||
const raw = rr && rr.text;
|
||||
let cards = [];
|
||||
if (raw) {
|
||||
let s = raw.replace(/```(?:json)?/gi, '').trim();
|
||||
|
||||
Reference in New Issue
Block a user