feat(trainer): P13 — конструктор параметрических генераторов

- custom_generators (мигр.084, spec_json + draft/published); customGeneratorController: validateGenSpec без исполнения (лимиты/типы), CRUD own+published + ownership
- /api/practice/generators[/:id]; клиент LS.practiceGen*
- страница /trainer-builder (учитель): форма (pick/derive/lhs/rhs/display/answer/solution) + живое превью через TE.instantiate(strict) (материализация + проверка ответа подстановкой) + список своих (правка/удаление/публикация)
- тренажёр грузит свои+опубликованные генераторы в тему «Мои генераторы» (пошаговый режим работает); пункт сайдбара /trainer-builder (teacher-only)
- тесты custom-generators.test.js 12/12; смоук движка 402/402 (T17 кастомный спек + strict-валидация); страница 33/33; ROADMAP_V2 P13 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 15:30:08 +03:00
parent 47d4f71eac
commit 6d600ad576
9 changed files with 694 additions and 7 deletions
+6
View File
@@ -1185,6 +1185,7 @@ window.LS = {
customSimShare, customSimClone, customSimRelated, customSimAddLink, customSimDelLink,
gameProgressList, gameProgressSubmit,
practiceProgressList, practiceSubmit, practicePool, practiceGenerate, practiceClassStats, practiceAuthor, practiceAssign,
practiceGenList, practiceGenGet, practiceGenCreate, practiceGenUpdate, practiceGenDelete,
assistantContext, assistantSeen, assistantDismiss, assistantSettings, assistantAsk, assistantAskStream, assistantFlashcards, assistantQuestions, assistantFeedback, assistantMemory, assistantMemoryClear, imageGen, imageGenStatus,
adminGetAssistant, adminSaveAssistant, adminTestAssistant, adminReindexTextbooks,
adminSaveProvider, adminDeleteProvider, adminSetActiveProvider, adminAssistantModels,
@@ -1427,6 +1428,11 @@ async function practiceGenerate(topic) { return req('POST', '/practice/genera
async function practiceClassStats(classId) { return req('GET', '/practice/class-stats?class_id=' + encodeURIComponent(classId)); }
async function practiceAuthor(data) { return req('POST', '/practice/author', data); }
async function practiceAssign(classId, topic, title) { return req('POST', '/practice/assign', { class_id: classId, topic: topic || 'word-linear', title }); }
async function practiceGenList() { return req('GET', '/practice/generators'); }
async function practiceGenGet(id) { return req('GET', '/practice/generators/' + id); }
async function practiceGenCreate(spec, status) { return req('POST', '/practice/generators', { spec, status }); }
async function practiceGenUpdate(id, spec, status) { return req('PUT', '/practice/generators/' + id, { spec, status }); }
async function practiceGenDelete(id) { return req('DELETE', '/practice/generators/' + id); }
async function assistantContext() { return req('GET', '/assistant/context'); }
async function assistantSeen(ruleId) { return req('POST', '/assistant/seen', { ruleId }); }
async function assistantDismiss(rid) { return req('POST', '/assistant/dismiss', { ruleId: rid }); }
+1
View File
@@ -90,6 +90,7 @@
${G('practice', 'Практика и игры', `
${L('/lab', 'atom', 'Лаборатория')}
${L('/trainer', 'dumbbell', 'Тренажёр')}
${L('/trainer-builder', 'wand-2', 'Конструктор задач', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/quantik', 'rocket', 'Квантик: Законы Мира')}
${L('/sim-builder', 'pencil-ruler', 'Конструктор симуляций', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/biochem', 'flask-conical', 'Биохимия')}