feat: Update Curriculum Studio with AI Assets panel to manually generate and input NotebookLM assets
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user