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;
|
return;
|
||||||
}
|
}
|
||||||
$content = CurriculumService::getLessonMarkdown($file);
|
$content = CurriculumService::getLessonMarkdown($file);
|
||||||
|
$assets = CurriculumService::getLessonAiAssets($file);
|
||||||
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'file' => $file,
|
'file' => $file,
|
||||||
'server_full_path' => $serverFullPath,
|
'server_full_path' => $serverFullPath,
|
||||||
'content' => $content
|
'content' => $content,
|
||||||
|
'ai_assets' => $assets
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Save Lesson Markdown Content
|
|
||||||
*/
|
|
||||||
public function saveLessonContent(Request $request, Response $response): void
|
public function saveLessonContent(Request $request, Response $response): void
|
||||||
{
|
{
|
||||||
$body = $request->getBody();
|
$body = $request->getBody();
|
||||||
$file = $body['file'] ?? '';
|
$file = $body['file'] ?? '';
|
||||||
$content = $body['content'] ?? '';
|
$content = $body['content'] ?? '';
|
||||||
|
$aiAssets = $body['ai_assets'] ?? null;
|
||||||
|
|
||||||
if (empty($file) || empty($content)) {
|
if (empty($file) || empty($content)) {
|
||||||
$response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']);
|
$response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']);
|
||||||
@@ -196,6 +196,10 @@ class CurriculumController
|
|||||||
}
|
}
|
||||||
|
|
||||||
CurriculumService::saveLessonMarkdown($file, $content);
|
CurriculumService::saveLessonMarkdown($file, $content);
|
||||||
|
if ($aiAssets !== null) {
|
||||||
|
CurriculumService::saveLessonAiAssets($file, $aiAssets);
|
||||||
|
}
|
||||||
|
|
||||||
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
@@ -204,4 +208,34 @@ class CurriculumController
|
|||||||
'server_full_path' => $serverFullPath
|
'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.
|
* 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
|
public static function enhanceLesson(int $lessonId): bool
|
||||||
{
|
{
|
||||||
$lesson = Database::selectOne(
|
$lesson = Database::selectOne(
|
||||||
|
|||||||
@@ -156,6 +156,31 @@ class CurriculumService
|
|||||||
return "# محتوى المنهاج\nالمحتوى المعتمد للمنهاج الرسمي.";
|
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
|
* Fast Keyword Search across All Markdown Files
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -197,7 +197,31 @@ class CurriculumStudio
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Markdown Content Area -->
|
<!-- 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>
|
</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() {
|
function openDirectLogViewer() {
|
||||||
const overlay = document.getElementById('upload_progress_overlay');
|
const overlay = document.getElementById('upload_progress_overlay');
|
||||||
overlay.style.display = 'flex';
|
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/tree', [\App\Controllers\CurriculumController::class, 'getTree']);
|
||||||
$router->get('/api/curriculum/lesson', [\App\Controllers\CurriculumController::class, 'getLessonContent']);
|
$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/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) {
|
$router->get('/api/curriculum/search', function ($request, $response) {
|
||||||
$q = $request->getQueryParams()['q'] ?? '';
|
$q = $request->getQueryParams()['q'] ?? '';
|
||||||
$response->json([
|
$response->json([
|
||||||
|
|||||||
Reference in New Issue
Block a user