Seed the vendor catalogue into SQL and add an optional AI course advisor
Course metadata: a scan now gives every course a course_meta row from the downloader's sidecar JSON when the folder has one, else from catalog_courses matched by the folder name the downloader would create (folderKeyFor, a port of ine.py's sanitize_name). tools/build-catalog-seed.php turns the INE export into seeds/ine_catalog.sql, an idempotent patch that creates its own table and upserts by id, so it can be applied to the live server before or after the code and re-run freely. Course pages show the blurb, difficulty, vendor and tags; search matches tags and blurbs; Settings reports how each course got its details. Against the live library 163 of 179 courses match. Course advisor, off by default: api/advisor.php answers "which course should I take to learn X / what comes before Y" with courses from the library, in order, plus vendor courses not yet downloaded. includes/ai_client.php is a small curl wrapper for any OpenAI-format chat endpoint (DeepSeek V4 Flash by default, no composer); the prompt is a byte-stable catalogue of the library so the provider's prefix cache covers it. Every course id the model returns is validated server-side, a bad JSON reply gets one retry then a plain-text fallback, and a per-user daily cap with token counts lives in advisor_requests. With AI_ENABLED false the app makes no outbound requests and the box does not appear; users can also hide it per browser. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012enquL2RZdjy3D5AcKP5yx
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
// GET api/library.php?view=settings -> library paths, thresholds, progress summary
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
require_once __DIR__ . '/../includes/advisor.php'; // aiEnabled() and the usage counters only
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$userId = requireLoginApi();
|
||||
@@ -95,6 +96,7 @@ switch ($view) {
|
||||
],
|
||||
'last_scan' => appMetaGet('last_scan_at'),
|
||||
'username' => currentUsername(),
|
||||
'advisor' => aiEnabled(),
|
||||
]);
|
||||
break;
|
||||
|
||||
@@ -179,6 +181,9 @@ switch ($view) {
|
||||
'rollup' => $rollup,
|
||||
'progress' => ['total' => $rollup['total'], 'completed' => $rollup['completed'],
|
||||
'percent' => $rollup['percent']],
|
||||
// What the catalogue or the sidecar file says about a course; null
|
||||
// for sections, categories and courses that came from elsewhere.
|
||||
'meta' => $kind === 'course' ? courseMetaPublic(courseMeta((int)$folder['id'])) : null,
|
||||
'categories' => $categories,
|
||||
'courses' => $courses,
|
||||
'subfolders' => $subfolders,
|
||||
@@ -282,6 +287,14 @@ switch ($view) {
|
||||
'progress_courses' => count($withSome),
|
||||
'courses' => count($courses),
|
||||
'shelf_counts' => $counts,
|
||||
'catalog_courses' => catalogCourseCount(),
|
||||
'course_meta_counts' => json_decode((string)appMetaGet('course_meta_counts'), true) ?: null,
|
||||
'advisor' => [
|
||||
'enabled' => aiEnabled(),
|
||||
'model' => aiEnabled() ? aiModel() : null,
|
||||
'used_today' => aiEnabled() ? advisorUsedToday($userId) : 0,
|
||||
'limit' => advisorLimit(),
|
||||
],
|
||||
]);
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user