extract learning hub ai panel controller
This commit is contained in:
parent
48931e8024
commit
e5e0435b64
5 changed files with 289 additions and 103 deletions
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from './learningHub/tiptapEditor.js';
|
} from './learningHub/tiptapEditor.js';
|
||||||
import { esc, sanitizeHtml } from './learningHub/sanitize.js';
|
import { esc, sanitizeHtml } from './learningHub/sanitize.js';
|
||||||
import { deleteJson, getJson, sendJson } from './learningHub/api.js';
|
import { deleteJson, getJson, sendJson } from './learningHub/api.js';
|
||||||
|
import { createAiPanelController } from './learningHub/aiPanelController.js';
|
||||||
import {
|
import {
|
||||||
renderOptionRowShell,
|
renderOptionRowShell,
|
||||||
renderQuestionBlockShell
|
renderQuestionBlockShell
|
||||||
|
|
@ -44,6 +45,12 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
||||||
showToast: function(message, type) { showToast(message, type); }
|
showToast: function(message, type) { showToast(message, type); }
|
||||||
});
|
});
|
||||||
var webdav = createWebdavController({});
|
var webdav = createWebdavController({});
|
||||||
|
var aiPanel = createAiPanelController({
|
||||||
|
browseWebdav: function(path) { webdav.browse(path); },
|
||||||
|
getEditorType: function() { return (document.getElementById('lh-cms-edit-type') || {}).value; },
|
||||||
|
getWebdavPath: function() { return webdav.getCurrentPath(); },
|
||||||
|
setWebdavPath: function(path) { webdav.setCurrentPath(path); }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Load when tab activated ────────────────────────────────
|
// ── Load when tab activated ────────────────────────────────
|
||||||
document.addEventListener('tabChanged', function(e) {
|
document.addEventListener('tabChanged', function(e) {
|
||||||
|
|
@ -117,8 +124,8 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
||||||
if (e.target.closest('#btn-lh-download-pptx')) { slides.downloadPptx(); return; }
|
if (e.target.closest('#btn-lh-download-pptx')) { slides.downloadPptx(); return; }
|
||||||
|
|
||||||
// AI panel
|
// AI panel
|
||||||
if (e.target.closest('#btn-lh-ai-open')) { openAiPanel(); return; }
|
if (e.target.closest('#btn-lh-ai-open')) { aiPanel.open(); return; }
|
||||||
if (e.target.closest('#btn-lh-ai-close')) { closeAiPanel(); return; }
|
if (e.target.closest('#btn-lh-ai-close')) { aiPanel.close(); return; }
|
||||||
if (e.target.closest('#btn-lh-ai-generate')) { runAiGenerate(); return; }
|
if (e.target.closest('#btn-lh-ai-generate')) { runAiGenerate(); return; }
|
||||||
if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; }
|
if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; }
|
||||||
if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; }
|
if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; }
|
||||||
|
|
@ -128,7 +135,7 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
||||||
|
|
||||||
// AI tabs
|
// AI tabs
|
||||||
var aiTab = e.target.closest('.lh-ai-tab');
|
var aiTab = e.target.closest('.lh-ai-tab');
|
||||||
if (aiTab && aiTab.dataset.aitab) { switchAiTab(aiTab.dataset.aitab); return; }
|
if (aiTab && aiTab.dataset.aitab) { aiPanel.switchTab(aiTab.dataset.aitab); return; }
|
||||||
|
|
||||||
// WebDAV list items
|
// WebDAV list items
|
||||||
var wdItem = e.target.closest('.lh-webdav-item');
|
var wdItem = e.target.closest('.lh-webdav-item');
|
||||||
|
|
@ -438,104 +445,6 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
||||||
// AI GENERATE PANEL
|
// AI GENERATE PANEL
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function openAiPanel() {
|
|
||||||
var panel = document.getElementById('lh-ai-panel');
|
|
||||||
if (!panel) return;
|
|
||||||
panel.classList.remove('hidden');
|
|
||||||
populateAiModelSelect();
|
|
||||||
|
|
||||||
// Sync content type selector with the editor's current type
|
|
||||||
var editorType = (document.getElementById('lh-cms-edit-type') || {}).value || 'article';
|
|
||||||
var ctype = document.getElementById('lh-ai-ctype');
|
|
||||||
if (ctype) ctype.value = editorType;
|
|
||||||
updateAiOptions(editorType);
|
|
||||||
|
|
||||||
// Hide WebDAV tab if Nextcloud not configured
|
|
||||||
getJson('/api/auth/me')
|
|
||||||
.then(function(d) {
|
|
||||||
var tab = document.getElementById('lh-ai-tab-webdav');
|
|
||||||
if (tab) tab.style.display = (d.user && d.user.nextcloud_url) ? '' : 'none';
|
|
||||||
if (d.user && d.user.webdav_learning_path) webdav.setCurrentPath(d.user.webdav_learning_path || '/');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wire ctype change once (use a flag so it doesn't stack on re-opens)
|
|
||||||
var ctypeEl = document.getElementById('lh-ai-ctype');
|
|
||||||
var toggleEl = document.getElementById('lh-ai-q-toggle');
|
|
||||||
if (ctypeEl && !ctypeEl._wired) {
|
|
||||||
ctypeEl.addEventListener('change', function() { updateAiOptions(ctypeEl.value); });
|
|
||||||
ctypeEl._wired = true;
|
|
||||||
}
|
|
||||||
if (toggleEl && !toggleEl._wired) {
|
|
||||||
toggleEl.addEventListener('change', function() { updateAiOptions((document.getElementById('lh-ai-ctype') || {}).value); });
|
|
||||||
toggleEl._wired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAiOptions(type) {
|
|
||||||
var wordsField = document.getElementById('lh-ai-words-field');
|
|
||||||
var slidesField = document.getElementById('lh-ai-slides-field');
|
|
||||||
var quizRow = document.getElementById('lh-ai-quiz-row');
|
|
||||||
var cardTitle = document.getElementById('lh-ai-quiz-card-title');
|
|
||||||
var toggleLabel = document.getElementById('lh-ai-q-toggle-label');
|
|
||||||
var toggleCb = document.getElementById('lh-ai-q-toggle');
|
|
||||||
var countWrap = document.getElementById('lh-ai-q-count-wrap');
|
|
||||||
|
|
||||||
// Use style.display directly — classList.toggle('hidden') doesn't work because
|
|
||||||
// .lh-ai-opt-field { display:flex } is defined later in CSS and overrides .hidden
|
|
||||||
if (wordsField) wordsField.style.display = (type === 'quiz' || type === 'presentation') ? 'none' : 'flex';
|
|
||||||
if (slidesField) slidesField.style.display = (type === 'presentation') ? 'flex' : 'none';
|
|
||||||
|
|
||||||
if (type === 'quiz') {
|
|
||||||
// Quiz: questions are the whole point — show the card prominently, no toggle needed
|
|
||||||
if (quizRow) quizRow.style.display = 'block';
|
|
||||||
if (cardTitle) cardTitle.textContent = 'Quiz Questions';
|
|
||||||
if (toggleLabel) toggleLabel.style.display = 'none';
|
|
||||||
if (toggleCb) { toggleCb.checked = true; toggleCb.disabled = true; }
|
|
||||||
if (countWrap) countWrap.style.display = 'flex';
|
|
||||||
} else if (type === 'presentation') {
|
|
||||||
// Presentation: optional questions (same as article)
|
|
||||||
if (quizRow) quizRow.style.display = 'block';
|
|
||||||
if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)';
|
|
||||||
if (toggleLabel) toggleLabel.style.display = 'flex';
|
|
||||||
if (toggleCb) toggleCb.disabled = false;
|
|
||||||
var checkedP = toggleCb ? toggleCb.checked : false;
|
|
||||||
if (countWrap) countWrap.style.display = checkedP ? 'flex' : 'none';
|
|
||||||
} else {
|
|
||||||
// Article / pearl: optional questions via toggle
|
|
||||||
if (quizRow) quizRow.style.display = 'block';
|
|
||||||
if (cardTitle) cardTitle.textContent = 'Quiz Questions (optional)';
|
|
||||||
if (toggleLabel) toggleLabel.style.display = 'flex';
|
|
||||||
if (toggleCb) toggleCb.disabled = false;
|
|
||||||
var checked = toggleCb ? toggleCb.checked : false;
|
|
||||||
if (countWrap) countWrap.style.display = checked ? 'flex' : 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAiPanel() {
|
|
||||||
var panel = document.getElementById('lh-ai-panel');
|
|
||||||
if (panel) panel.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateAiModelSelect() {
|
|
||||||
var sel = document.getElementById('lh-ai-model');
|
|
||||||
if (!sel || sel.options.length > 1) return;
|
|
||||||
if (typeof window._buildModelOptions === 'function') {
|
|
||||||
window._buildModelOptions(sel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchAiTab(tabName) {
|
|
||||||
document.querySelectorAll('.lh-ai-tab').forEach(function(t) { t.classList.toggle('active', t.dataset.aitab === tabName); });
|
|
||||||
document.querySelectorAll('.lh-ai-tabpanel').forEach(function(p) { p.classList.add('hidden'); });
|
|
||||||
var tp = document.getElementById('lh-ai-tp-' + tabName);
|
|
||||||
if (tp) tp.classList.remove('hidden');
|
|
||||||
if (tabName === 'webdav' && !document.querySelector('#lh-ai-webdav-list .lh-webdav-item')) {
|
|
||||||
webdav.browse(webdav.getCurrentPath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Generate ────────────────────────────────────────────────
|
// ── Generate ────────────────────────────────────────────────
|
||||||
function runAiGenerate() {
|
function runAiGenerate() {
|
||||||
var activeTab = document.querySelector('.lh-ai-tab.active');
|
var activeTab = document.querySelector('.lh-ai-tab.active');
|
||||||
|
|
@ -596,7 +505,7 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
||||||
// Presentation returns marpMarkdown directly; others return content{}
|
// Presentation returns marpMarkdown directly; others return content{}
|
||||||
var payload = data.contentType === 'presentation' ? data : data.content;
|
var payload = data.contentType === 'presentation' ? data : data.content;
|
||||||
return applyAiContent(payload, contentType).then(function() {
|
return applyAiContent(payload, contentType).then(function() {
|
||||||
closeAiPanel();
|
aiPanel.close();
|
||||||
showToast('Content generated! Review and save.', 'success');
|
showToast('Content generated! Review and save.', 'success');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
103
public/js/learningHub/aiPanelController.js
Normal file
103
public/js/learningHub/aiPanelController.js
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { getJson } from './api.js';
|
||||||
|
|
||||||
|
export function createAiPanelController(deps) {
|
||||||
|
function open() {
|
||||||
|
var panel = document.getElementById('lh-ai-panel');
|
||||||
|
if (!panel) return;
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
populateModelSelect();
|
||||||
|
|
||||||
|
var editorType = deps.getEditorType() || 'article';
|
||||||
|
var ctype = document.getElementById('lh-ai-ctype');
|
||||||
|
if (ctype) ctype.value = editorType;
|
||||||
|
updateOptions(editorType);
|
||||||
|
|
||||||
|
getJson('/api/auth/me')
|
||||||
|
.then(function(d) {
|
||||||
|
var tab = document.getElementById('lh-ai-tab-webdav');
|
||||||
|
if (tab) tab.style.display = (d.user && d.user.nextcloud_url) ? '' : 'none';
|
||||||
|
if (d.user && d.user.webdav_learning_path) deps.setWebdavPath(d.user.webdav_learning_path || '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
wireOptionControls();
|
||||||
|
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
var panel = document.getElementById('lh-ai-panel');
|
||||||
|
if (panel) panel.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateModelSelect() {
|
||||||
|
var sel = document.getElementById('lh-ai-model');
|
||||||
|
if (!sel || sel.options.length > 1) return;
|
||||||
|
if (typeof window._buildModelOptions === 'function') window._buildModelOptions(sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.lh-ai-tab').forEach(function(t) {
|
||||||
|
t.classList.toggle('active', t.dataset.aitab === tabName);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.lh-ai-tabpanel').forEach(function(p) { p.classList.add('hidden'); });
|
||||||
|
var tp = document.getElementById('lh-ai-tp-' + tabName);
|
||||||
|
if (tp) tp.classList.remove('hidden');
|
||||||
|
if (tabName === 'webdav' && !document.querySelector('#lh-ai-webdav-list .lh-webdav-item')) {
|
||||||
|
deps.browseWebdav(deps.getWebdavPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOptions(type) {
|
||||||
|
applyAiOptionState(type, {
|
||||||
|
wordsField: document.getElementById('lh-ai-words-field'),
|
||||||
|
slidesField: document.getElementById('lh-ai-slides-field'),
|
||||||
|
quizRow: document.getElementById('lh-ai-quiz-row'),
|
||||||
|
cardTitle: document.getElementById('lh-ai-quiz-card-title'),
|
||||||
|
toggleLabel: document.getElementById('lh-ai-q-toggle-label'),
|
||||||
|
toggleCb: document.getElementById('lh-ai-q-toggle'),
|
||||||
|
countWrap: document.getElementById('lh-ai-q-count-wrap')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireOptionControls() {
|
||||||
|
var ctypeEl = document.getElementById('lh-ai-ctype');
|
||||||
|
var toggleEl = document.getElementById('lh-ai-q-toggle');
|
||||||
|
if (ctypeEl && !ctypeEl._wired) {
|
||||||
|
ctypeEl.addEventListener('change', function() { updateOptions(ctypeEl.value); });
|
||||||
|
ctypeEl._wired = true;
|
||||||
|
}
|
||||||
|
if (toggleEl && !toggleEl._wired) {
|
||||||
|
toggleEl.addEventListener('change', function() {
|
||||||
|
updateOptions((document.getElementById('lh-ai-ctype') || {}).value);
|
||||||
|
});
|
||||||
|
toggleEl._wired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: close,
|
||||||
|
open: open,
|
||||||
|
switchTab: switchTab,
|
||||||
|
updateOptions: updateOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAiOptionState(type, els) {
|
||||||
|
if (els.wordsField) els.wordsField.style.display = (type === 'quiz' || type === 'presentation') ? 'none' : 'flex';
|
||||||
|
if (els.slidesField) els.slidesField.style.display = type === 'presentation' ? 'flex' : 'none';
|
||||||
|
|
||||||
|
if (type === 'quiz') {
|
||||||
|
if (els.quizRow) els.quizRow.style.display = 'block';
|
||||||
|
if (els.cardTitle) els.cardTitle.textContent = 'Quiz Questions';
|
||||||
|
if (els.toggleLabel) els.toggleLabel.style.display = 'none';
|
||||||
|
if (els.toggleCb) { els.toggleCb.checked = true; els.toggleCb.disabled = true; }
|
||||||
|
if (els.countWrap) els.countWrap.style.display = 'flex';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (els.quizRow) els.quizRow.style.display = 'block';
|
||||||
|
if (els.cardTitle) els.cardTitle.textContent = 'Quiz Questions (optional)';
|
||||||
|
if (els.toggleLabel) els.toggleLabel.style.display = 'flex';
|
||||||
|
if (els.toggleCb) els.toggleCb.disabled = false;
|
||||||
|
var checked = els.toggleCb ? els.toggleCb.checked : false;
|
||||||
|
if (els.countWrap) els.countWrap.style.display = checked ? 'flex' : 'none';
|
||||||
|
}
|
||||||
156
test/learning-hub-ai-panel-controller.test.js
Normal file
156
test/learning-hub-ai-panel-controller.test.js
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { pathToFileURL } = require('node:url');
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
async function loadAiPanelController() {
|
||||||
|
return import(pathToFileURL(path.join(__dirname, '..', 'public/js/learningHub/aiPanelController.js')).href);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDom() {
|
||||||
|
return new JSDOM(`<!doctype html>
|
||||||
|
<div id="lh-ai-panel" class="hidden"></div>
|
||||||
|
<select id="lh-ai-model"><option value="">Default</option></select>
|
||||||
|
<select id="lh-ai-ctype"><option value="article">Article</option><option value="quiz">Quiz</option><option value="presentation">Presentation</option></select>
|
||||||
|
<div id="lh-ai-words-field"></div>
|
||||||
|
<div id="lh-ai-slides-field"></div>
|
||||||
|
<div id="lh-ai-quiz-row"></div>
|
||||||
|
<div id="lh-ai-quiz-card-title"></div>
|
||||||
|
<label id="lh-ai-q-toggle-label"><input id="lh-ai-q-toggle" type="checkbox"></label>
|
||||||
|
<div id="lh-ai-q-count-wrap"></div>
|
||||||
|
<button id="lh-ai-tab-prompt" class="lh-ai-tab active" data-aitab="prompt"></button>
|
||||||
|
<button id="lh-ai-tab-webdav" class="lh-ai-tab" data-aitab="webdav"></button>
|
||||||
|
<div id="lh-ai-tp-prompt" class="lh-ai-tabpanel"></div>
|
||||||
|
<div id="lh-ai-tp-webdav" class="lh-ai-tabpanel hidden"></div>
|
||||||
|
<div id="lh-ai-webdav-list"></div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withGlobals(dom, fn) {
|
||||||
|
const previousDocument = global.document;
|
||||||
|
const previousWindow = global.window;
|
||||||
|
const previousFetch = global.fetch;
|
||||||
|
const previousGetAuthHeaders = global.getAuthHeaders;
|
||||||
|
|
||||||
|
global.document = dom.window.document;
|
||||||
|
global.window = dom.window;
|
||||||
|
global.getAuthHeaders = function() { return {}; };
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(fn)
|
||||||
|
.finally(function() {
|
||||||
|
global.document = previousDocument;
|
||||||
|
global.window = previousWindow;
|
||||||
|
global.fetch = previousFetch;
|
||||||
|
global.getAuthHeaders = previousGetAuthHeaders;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Learning Hub AI panel option state matches content type semantics', async () => {
|
||||||
|
const { applyAiOptionState } = await loadAiPanelController();
|
||||||
|
const els = {
|
||||||
|
wordsField: { style: {} },
|
||||||
|
slidesField: { style: {} },
|
||||||
|
quizRow: { style: {} },
|
||||||
|
cardTitle: { textContent: '' },
|
||||||
|
toggleLabel: { style: {} },
|
||||||
|
toggleCb: { checked: false, disabled: false },
|
||||||
|
countWrap: { style: {} }
|
||||||
|
};
|
||||||
|
|
||||||
|
applyAiOptionState('article', els);
|
||||||
|
assert.equal(els.wordsField.style.display, 'flex');
|
||||||
|
assert.equal(els.slidesField.style.display, 'none');
|
||||||
|
assert.equal(els.cardTitle.textContent, 'Quiz Questions (optional)');
|
||||||
|
assert.equal(els.countWrap.style.display, 'none');
|
||||||
|
|
||||||
|
els.toggleCb.checked = true;
|
||||||
|
applyAiOptionState('presentation', els);
|
||||||
|
assert.equal(els.wordsField.style.display, 'none');
|
||||||
|
assert.equal(els.slidesField.style.display, 'flex');
|
||||||
|
assert.equal(els.toggleCb.disabled, false);
|
||||||
|
assert.equal(els.countWrap.style.display, 'flex');
|
||||||
|
|
||||||
|
els.toggleCb.checked = false;
|
||||||
|
applyAiOptionState('quiz', els);
|
||||||
|
assert.equal(els.wordsField.style.display, 'none');
|
||||||
|
assert.equal(els.slidesField.style.display, 'none');
|
||||||
|
assert.equal(els.cardTitle.textContent, 'Quiz Questions');
|
||||||
|
assert.equal(els.toggleLabel.style.display, 'none');
|
||||||
|
assert.equal(els.toggleCb.checked, true);
|
||||||
|
assert.equal(els.toggleCb.disabled, true);
|
||||||
|
assert.equal(els.countWrap.style.display, 'flex');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Learning Hub AI panel opens with defaults, models, auth state, and stable handlers', async () => {
|
||||||
|
const dom = makeDom();
|
||||||
|
await withGlobals(dom, async function() {
|
||||||
|
const { createAiPanelController } = await loadAiPanelController();
|
||||||
|
let modelBuilds = 0;
|
||||||
|
let fetches = 0;
|
||||||
|
const paths = [];
|
||||||
|
const panel = document.getElementById('lh-ai-panel');
|
||||||
|
panel.scrollIntoView = function() {};
|
||||||
|
window._buildModelOptions = function(sel) {
|
||||||
|
modelBuilds += 1;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = 'model-a';
|
||||||
|
opt.textContent = 'Model A';
|
||||||
|
sel.appendChild(opt);
|
||||||
|
};
|
||||||
|
global.fetch = function(url) {
|
||||||
|
fetches += 1;
|
||||||
|
assert.equal(url, '/api/auth/me');
|
||||||
|
return Promise.resolve({ json: function() { return Promise.resolve({ user: { nextcloud_url: 'https://cloud.example', webdav_learning_path: '/Learning' } }); } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const controller = createAiPanelController({
|
||||||
|
browseWebdav: function() {},
|
||||||
|
getEditorType: function() { return 'presentation'; },
|
||||||
|
getWebdavPath: function() { return '/Learning'; },
|
||||||
|
setWebdavPath: function(pathValue) { paths.push(pathValue); }
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.open();
|
||||||
|
controller.open();
|
||||||
|
await new Promise(function(resolve) { setImmediate(resolve); });
|
||||||
|
|
||||||
|
assert.equal(panel.classList.contains('hidden'), false);
|
||||||
|
assert.equal(document.getElementById('lh-ai-ctype').value, 'presentation');
|
||||||
|
assert.equal(document.getElementById('lh-ai-words-field').style.display, 'none');
|
||||||
|
assert.equal(document.getElementById('lh-ai-slides-field').style.display, 'flex');
|
||||||
|
assert.equal(document.getElementById('lh-ai-tab-webdav').style.display, '');
|
||||||
|
assert.deepEqual(paths, ['/Learning', '/Learning']);
|
||||||
|
assert.equal(modelBuilds, 1);
|
||||||
|
assert.equal(fetches, 2);
|
||||||
|
|
||||||
|
const ctype = document.getElementById('lh-ai-ctype');
|
||||||
|
ctype.value = 'quiz';
|
||||||
|
ctype.dispatchEvent(new window.Event('change'));
|
||||||
|
assert.equal(document.getElementById('lh-ai-q-toggle').disabled, true);
|
||||||
|
assert.equal(document.getElementById('lh-ai-quiz-card-title').textContent, 'Quiz Questions');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Learning Hub AI panel tab switch lazily browses empty WebDAV panel', async () => {
|
||||||
|
const dom = makeDom();
|
||||||
|
await withGlobals(dom, async function() {
|
||||||
|
const { createAiPanelController } = await loadAiPanelController();
|
||||||
|
const browsedPaths = [];
|
||||||
|
const controller = createAiPanelController({
|
||||||
|
browseWebdav: function(pathValue) { browsedPaths.push(pathValue); },
|
||||||
|
getEditorType: function() { return 'article'; },
|
||||||
|
getWebdavPath: function() { return '/Learning/Sub'; },
|
||||||
|
setWebdavPath: function() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.switchTab('webdav');
|
||||||
|
assert.equal(document.getElementById('lh-ai-tab-webdav').classList.contains('active'), true);
|
||||||
|
assert.equal(document.getElementById('lh-ai-tp-webdav').classList.contains('hidden'), false);
|
||||||
|
assert.deepEqual(browsedPaths, ['/Learning/Sub']);
|
||||||
|
|
||||||
|
document.getElementById('lh-ai-webdav-list').innerHTML = '<button class="lh-webdav-item"></button>';
|
||||||
|
controller.switchTab('webdav');
|
||||||
|
assert.deepEqual(browsedPaths, ['/Learning/Sub']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,6 +9,7 @@ function read(relativePath) {
|
||||||
|
|
||||||
const source = read('public/js/learningHub.js');
|
const source = read('public/js/learningHub.js');
|
||||||
const apiModule = read('public/js/learningHub/api.js');
|
const apiModule = read('public/js/learningHub/api.js');
|
||||||
|
const aiPanelControllerModule = read('public/js/learningHub/aiPanelController.js');
|
||||||
const cmsControllerModule = read('public/js/learningHub/cmsController.js');
|
const cmsControllerModule = read('public/js/learningHub/cmsController.js');
|
||||||
const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
|
const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
|
||||||
const editorModule = read('public/js/learningHub/tiptapEditor.js');
|
const editorModule = read('public/js/learningHub/tiptapEditor.js');
|
||||||
|
|
@ -164,6 +165,21 @@ test('Learning Hub WebDAV controller owns browser state and rendering', () => {
|
||||||
assert.doesNotMatch(webdavControllerModule, /window\.Tiptap|ai-generate|FormData/);
|
assert.doesNotMatch(webdavControllerModule, /window\.Tiptap|ai-generate|FormData/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Learning Hub AI panel controller owns panel options and source tabs', () => {
|
||||||
|
assert.match(source, /from '\.\/learningHub\/aiPanelController\.js'/);
|
||||||
|
assert.match(source, /var aiPanel = createAiPanelController\(/);
|
||||||
|
assert.match(source, /aiPanel\.open\(\)/);
|
||||||
|
assert.match(source, /aiPanel\.close\(\)/);
|
||||||
|
assert.match(source, /aiPanel\.switchTab\(aiTab\.dataset\.aitab\)/);
|
||||||
|
assert.match(aiPanelControllerModule, /export function createAiPanelController\(deps\)/);
|
||||||
|
assert.match(aiPanelControllerModule, /export function applyAiOptionState\(type, els\)/);
|
||||||
|
assert.match(aiPanelControllerModule, /window\._buildModelOptions\(sel\)/);
|
||||||
|
assert.match(aiPanelControllerModule, /getJson\('\/api\/auth\/me'\)/);
|
||||||
|
assert.match(aiPanelControllerModule, /deps\.browseWebdav\(deps\.getWebdavPath\(\)\)/);
|
||||||
|
assert.doesNotMatch(source, /function openAiPanel\(\)|function updateAiOptions\(|function switchAiTab\(/);
|
||||||
|
assert.doesNotMatch(aiPanelControllerModule, /window\.Tiptap|ai-generate|FormData|generate-pptx/);
|
||||||
|
});
|
||||||
|
|
||||||
test('Learning Hub viewer renderers are isolated from API and slide loading', () => {
|
test('Learning Hub viewer renderers are isolated from API and slide loading', () => {
|
||||||
assert.match(source, /from '\.\/learningHub\/viewerRenderer\.js'/);
|
assert.match(source, /from '\.\/learningHub\/viewerRenderer\.js'/);
|
||||||
assert.match(viewerRendererModule, /export function buildViewerMeta\(item\)/);
|
assert.match(viewerRendererModule, /export function buildViewerMeta\(item\)/);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ test('top bar model selector is removed and omitted selections use backend defau
|
||||||
|
|
||||||
test('learning hub model selector uses shared tab model options', () => {
|
test('learning hub model selector uses shared tab model options', () => {
|
||||||
const learningHub = read('public/js/learningHub.js');
|
const learningHub = read('public/js/learningHub.js');
|
||||||
assert.match(learningHub, /window\._buildModelOptions\(sel\)/);
|
const aiPanelController = read('public/js/learningHub/aiPanelController.js');
|
||||||
|
assert.match(learningHub, /createAiPanelController\(/);
|
||||||
|
assert.match(aiPanelController, /window\._buildModelOptions\(sel\)/);
|
||||||
assert.doesNotMatch(learningHub, /global-model-select|globalSel\.innerHTML/);
|
assert.doesNotMatch(learningHub, /global-model-select|globalSel\.innerHTML/);
|
||||||
assert.match(learningHub, /if \(model\) formData\.append\('model', model\)/);
|
assert.match(learningHub, /if \(model\) formData\.append\('model', model\)/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue