feat: Update Curriculum Studio with AI Assets panel to manually generate and input NotebookLM assets

This commit is contained in:
Hamza-Ayed
2026-08-29 02:01:52 +03:00
parent 074c08fc6f
commit 135a379b32
5 changed files with 217 additions and 5 deletions
@@ -171,24 +171,24 @@ class CurriculumController
return;
}
$content = CurriculumService::getLessonMarkdown($file);
$assets = CurriculumService::getLessonAiAssets($file);
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
$response->json([
'status' => 'success',
'file' => $file,
'server_full_path' => $serverFullPath,
'content' => $content
'content' => $content,
'ai_assets' => $assets
]);
}
/**
* Save Lesson Markdown Content
*/
public function saveLessonContent(Request $request, Response $response): void
{
$body = $request->getBody();
$file = $body['file'] ?? '';
$content = $body['content'] ?? '';
$aiAssets = $body['ai_assets'] ?? null;
if (empty($file) || empty($content)) {
$response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']);
@@ -196,6 +196,10 @@ class CurriculumController
}
CurriculumService::saveLessonMarkdown($file, $content);
if ($aiAssets !== null) {
CurriculumService::saveLessonAiAssets($file, $aiAssets);
}
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
$response->json([
@@ -204,4 +208,34 @@ class CurriculumController
'server_full_path' => $serverFullPath
]);
}
public function generateAiAssets(Request $request, Response $response): void
{
$body = $request->getBody();
$file = $body['file'] ?? '';
if (empty($file)) {
$response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']);
return;
}
$content = CurriculumService::getLessonMarkdown($file);
if (empty($content)) {
$response->status(400)->json(['status' => 'error', 'message' => 'محتوى الدرس فارغ']);
return;
}
// Generate Assets
require_once __DIR__ . '/../Services/AiLessonEnhancerService.php';
// We will adapt the service call here since AiLessonEnhancerService expects lessonId.
// Actually, let's create a custom static method in AiLessonEnhancerService to accept raw text.
$assets = \App\Services\AiLessonEnhancerService::generateFromText('الدرس', $content);
if ($assets) {
CurriculumService::saveLessonAiAssets($file, $assets);
$response->json(['status' => 'success', 'ai_assets' => $assets]);
} else {
$response->status(500)->json(['status' => 'error', 'message' => 'فشل توليد المخرجات الذكية']);
}
}
}
@@ -10,6 +10,12 @@ class AiLessonEnhancerService
/**
* Extracts the lively script, cheat sheet, and Socratic quiz using Gemini.
*/
public static function generateFromText(string $title, string $content): ?array
{
$prompt = self::buildPrompt($title, $content);
return self::callGemini($prompt);
}
public static function enhanceLesson(int $lessonId): bool
{
$lesson = Database::selectOne(
@@ -156,6 +156,31 @@ class CurriculumService
return "# محتوى المنهاج\nالمحتوى المعتمد للمنهاج الرسمي.";
}
public static function saveLessonAiAssets(string $relativePath, array $assets): bool
{
self::ensureStorage();
$baseName = preg_replace('/\.md$/i', '', ltrim($relativePath, '/'));
$jsonPath = self::$storagePath . '/' . $baseName . '_ai_assets.json';
$dir = dirname($jsonPath);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
return file_put_contents($jsonPath, json_encode($assets, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) !== false;
}
public static function getLessonAiAssets(string $relativePath): array
{
self::ensureStorage();
$baseName = preg_replace('/\.md$/i', '', ltrim($relativePath, '/'));
$jsonPath = self::$storagePath . '/' . $baseName . '_ai_assets.json';
if (file_exists($jsonPath)) {
return json_decode(file_get_contents($jsonPath), true) ?: [];
}
return [];
}
/**
* Fast Keyword Search across All Markdown Files
*/
+147 -1
View File
@@ -197,7 +197,31 @@ class CurriculumStudio
</div>
<!-- Markdown Content Area -->
<textarea id="lesson_markdown_editor" class="md-editor"></textarea>
<textarea id="lesson_markdown_editor" class="md-editor" style="height: 400px; margin-bottom: 20px;"></textarea>
<!-- الذكاء الاصطناعي والمخرجات الذكية -->
<div style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="color: var(--accent-gold); font-size: 16px; margin: 0;">🧠 الاستوديو الذكي للدرس (AI Assets)</h4>
<button class="btn-primary" style="padding: 8px 16px; font-size: 13px;" onclick="generateAiAssets()">✨ توليد الملخص والاختبار آلياً</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">رابط فيديو الشرح الذكي (مثلاً من NotebookLM):</label>
<input type="text" id="ai_video_url_input" style="width: 100%; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 14px;" placeholder="https://...">
</div>
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">ملخص الدرس (Cheat Sheet):</label>
<textarea id="ai_cheat_sheet_editor" style="width: 100%; height: 150px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: rtl;" placeholder="سيتم توليد الملخص والبطاقات التعليمية هنا..."></textarea>
</div>
<div style="flex: 1;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">الفحص السقراطي (JSON):</label>
<textarea id="ai_socratic_quiz_editor" style="width: 100%; height: 150px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: ltr; text-align: left;" placeholder="سيتم توليد سؤال الفحص السقراطي هنا..."></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -487,6 +511,128 @@ class CurriculumStudio
}
}
// ==========================================================
// 3. Status Polling & Overlay
// ==========================================================
let pollInterval;
function checkStatus() {
pollInterval = setInterval(async () => {
const res = await fetch('/api/curriculum/status');
const state = await res.json();
if (state.status === 'processing') {
const pct = state.progress || 0;
document.getElementById('upload_progress_fill').style.width = pct + '%';
document.getElementById('upload_progress_text').textContent = state.message || 'جاري المعالجة...';
} else if (state.status === 'done') {
clearInterval(pollInterval);
document.getElementById('upload_progress_fill').style.width = '100%';
document.getElementById('upload_progress_text').textContent = '✅ اكتمل استخراج المنهاج بنجاح!';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
setTimeout(() => {
if (state.extracted_data) {
renderTree(state.extracted_data);
if (state.active_file) {
selectLesson(state.active_file, state.extracted_data?.subject_name || 'الدرس المستخرج', 'المنهاج المستخرج', []);
}
} else {
location.reload();
}
}, 2000);
} else if (state.status === 'error') {
document.getElementById('upload_progress_text').textContent = '⚠️ فشل: ' + state.message;
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
} else {
clearInterval(pollInterval);
}
}, 1500);
}
// ==========================================================
// 4. Save Content
// ==========================================================
async function saveLessonContent() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
let socraticJson = null;
const socraticRaw = document.getElementById('ai_socratic_quiz_editor').value.trim();
if (socraticRaw) {
try {
socraticJson = JSON.parse(socraticRaw);
} catch(e) {
alert('⚠️ خطأ في صيغة JSON الخاصة بالفحص السقراطي');
return;
}
}
const aiAssets = {
ai_video_url: document.getElementById('ai_video_url_input').value,
cheat_sheet: document.getElementById('ai_cheat_sheet_editor').value,
socratic_quiz: socraticJson
};
try {
const res = await fetch('/api/curriculum/save-lesson', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: currentFilePath, content: content, ai_assets: aiAssets })
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في الملف:\n${data.server_full_path || currentFilePath}`);
} else {
alert('⚠️ فشل حفظ الملف');
}
} catch (e) {
console.error(e);
alert('⚠️ حدث خطأ في الاتصال');
}
}
// ==========================================================
// 5. Generate AI Assets automatically
// ==========================================================
async function generateAiAssets() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
if (!content) {
alert('يجب أن يحتوي الدرس على نص أولاً');
return;
}
const btn = window.event.currentTarget;
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ جاري التوليد الذكي...';
btn.disabled = true;
try {
const res = await fetch('/api/curriculum/generate-ai-assets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: currentFilePath })
});
const data = await res.json();
if (res.ok && data.status === 'success' && data.ai_assets) {
document.getElementById('ai_cheat_sheet_editor').value = data.ai_assets.cheat_sheet || '';
document.getElementById('ai_socratic_quiz_editor').value = data.ai_assets.socratic_quiz ? JSON.stringify(data.ai_assets.socratic_quiz, null, 2) : '';
alert('✅ تم توليد الملخص والاختبار السقراطي بنجاح!');
} else {
alert('⚠️ فشل التوليد: ' + (data.message || 'خطأ غير معروف'));
}
} catch (e) {
console.error(e);
alert('⚠️ حدث خطأ في الاتصال بمحرك الذكاء الاصطناعي');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
function openDirectLogViewer() {
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
+1
View File
@@ -62,6 +62,7 @@ $router->get('/api/curriculum/upload-log', [\App\Controllers\CurriculumControlle
$router->get('/api/curriculum/tree', [\App\Controllers\CurriculumController::class, 'getTree']);
$router->get('/api/curriculum/lesson', [\App\Controllers\CurriculumController::class, 'getLessonContent']);
$router->post('/api/curriculum/save-lesson', [\App\Controllers\CurriculumController::class, 'saveLessonContent']);
$router->post('/api/curriculum/generate-ai-assets', [\App\Controllers\CurriculumController::class, 'generateAiAssets']);
$router->get('/api/curriculum/search', function ($request, $response) {
$q = $request->getQueryParams()['q'] ?? '';
$response->json([