//! Classroom generation commands //! //! - `classroom_generate` — start 4-stage pipeline, emit progress events //! - `classroom_generation_progress` — query current progress //! - `classroom_cancel_generation` — cancel active generation //! - `classroom_get` — retrieve generated classroom data //! - `classroom_list` — list all generated classrooms use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, State}; use zclaw_kernel::generation::{ Classroom, GenerationPipeline, GenerationRequest as KernelGenRequest, GenerationStage, TeachingStyle, DifficultyLevel, }; use super::{ClassroomStore, GenerationTasks}; use crate::kernel_commands::KernelState; // --------------------------------------------------------------------------- // Request / Response types // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClassroomGenerateRequest { pub topic: String, pub document: Option, pub style: Option, pub level: Option, pub target_duration_minutes: Option, pub scene_count: Option, pub custom_instructions: Option, pub language: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClassroomGenerateResponse { pub classroom_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClassroomProgressResponse { pub stage: String, pub progress: u8, pub activity: String, pub items_progress: Option<(usize, usize)>, } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- fn parse_style(s: Option<&str>) -> TeachingStyle { match s.unwrap_or("lecture") { "discussion" => TeachingStyle::Discussion, "pbl" => TeachingStyle::Pbl, "flipped" => TeachingStyle::Flipped, "socratic" => TeachingStyle::Socratic, _ => TeachingStyle::Lecture, } } fn parse_level(l: Option<&str>) -> DifficultyLevel { match l.unwrap_or("intermediate") { "beginner" => DifficultyLevel::Beginner, "advanced" => DifficultyLevel::Advanced, "expert" => DifficultyLevel::Expert, _ => DifficultyLevel::Intermediate, } } fn stage_name(stage: &GenerationStage) -> &'static str { match stage { GenerationStage::AgentProfiles => "agent_profiles", GenerationStage::Outline => "outline", GenerationStage::Scene => "scene", GenerationStage::Complete => "complete", } } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- /// Start classroom generation (4-stage pipeline). /// Progress events are emitted via `classroom:progress`. /// Supports cancellation between stages by removing the task from GenerationTasks. // @reserved: classroom generation // @connected #[tauri::command] pub async fn classroom_generate( app: AppHandle, store: State<'_, ClassroomStore>, tasks: State<'_, GenerationTasks>, kernel_state: State<'_, KernelState>, persistence: State<'_, crate::classroom_commands::persist::ClassroomPersistence>, request: ClassroomGenerateRequest, ) -> Result { if request.topic.trim().is_empty() { return Err("Topic is required".to_string()); } let topic_clone = request.topic.clone(); let kernel_request = KernelGenRequest { topic: request.topic.clone(), document: request.document.clone(), style: parse_style(request.style.as_deref()), level: parse_level(request.level.as_deref()), target_duration_minutes: request.target_duration_minutes.unwrap_or(30), scene_count: request.scene_count, custom_instructions: request.custom_instructions.clone(), language: request.language.clone().or_else(|| Some("zh-CN".to_string())), }; // Register generation task so cancellation can check it { use zclaw_kernel::generation::GenerationProgress; let mut t = tasks.lock().await; t.insert(topic_clone.clone(), GenerationProgress { stage: zclaw_kernel::generation::GenerationStage::AgentProfiles, progress: 0, activity: "Starting generation...".to_string(), items_progress: None, eta_seconds: None, }); } // Get LLM driver from kernel if available, otherwise use placeholder mode let (pipeline, has_driver) = { let ks = kernel_state.lock().await; if let Some(kernel) = ks.as_ref() { (GenerationPipeline::with_driver(kernel.driver(), kernel.config().model().to_string()), true) } else { (GenerationPipeline::new(), false) } }; // Helper: check if cancelled (try_lock avoids blocking the async runtime) let is_cancelled = || { match tasks.try_lock() { Ok(t) => !t.contains_key(&topic_clone), Err(_) => false, // Lock contested — treat as not cancelled } }; // Helper: emit progress event let emit_progress = |stage: &str, progress: u8, activity: &str| { let _ = app.emit("classroom:progress", serde_json::json!({ "topic": &topic_clone, "stage": stage, "progress": progress, "activity": activity })); }; // ── Stage 0: Agent Profiles ── emit_progress("agent_profiles", 5, "生成课堂角色..."); let agents = pipeline.generate_agent_profiles(&kernel_request).await; emit_progress("agent_profiles", 25, "角色生成完成"); if is_cancelled() { return Err("Generation cancelled".to_string()); } // ── Stage 1: Outline ── emit_progress("outline", 30, "分析主题,生成大纲..."); let outline = pipeline.generate_outline(&kernel_request).await .map_err(|e| format!("Outline generation failed: {}", e))?; emit_progress("outline", 50, &format!("大纲完成:{} 个场景", outline.len())); if is_cancelled() { return Err("Generation cancelled".to_string()); } // ── Stage 2: Scenes (parallel) ── emit_progress("scene", 55, &format!("并行生成 {} 个场景...", outline.len())); let scenes = pipeline.generate_scenes(&outline).await .map_err(|e| format!("Scene generation failed: {}", e))?; if is_cancelled() { return Err("Generation cancelled".to_string()); } // ── Stage 3: Assemble ── emit_progress("complete", 90, "组装课堂..."); // Build classroom directly (pipeline.build_classroom is private) let total_duration: u32 = scenes.iter().map(|s| s.content.duration_seconds).sum(); let objectives = outline.iter() .take(3) .map(|item| format!("理解: {}", item.title)) .collect::>(); let classroom_id = uuid::Uuid::new_v4().to_string(); let classroom = Classroom { id: classroom_id.clone(), title: format!("课堂: {}", kernel_request.topic), description: format!("{:?} 风格课堂 — {}", kernel_request.style, kernel_request.topic), topic: kernel_request.topic.clone(), style: kernel_request.style, level: kernel_request.level, total_duration, objectives, scenes, agents, metadata: zclaw_kernel::generation::ClassroomMetadata { generated_at: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as i64, source_document: kernel_request.document.map(|_| "user_document".to_string()), model: None, version: "2.0.0".to_string(), is_placeholder: !has_driver, // P2-10: true when no LLM driver available custom: serde_json::Map::new(), }, }; // Store classroom { let mut s = store.lock().await; s.insert(classroom_id.clone(), classroom.clone()); // Persist to SQLite if let Err(e) = persistence.save_classroom(&classroom).await { tracing::warn!("[ClassroomGenerate] Failed to persist classroom {}: {}", classroom_id, e); } } // Clear generation task { let mut t = tasks.lock().await; t.remove(&topic_clone); } // Emit completion emit_progress("complete", 100, "课堂生成完成"); Ok(ClassroomGenerateResponse { classroom_id, }) } /// @reserved — no frontend UI yet /// Get current generation progress for a topic #[tauri::command] pub async fn classroom_generation_progress( tasks: State<'_, GenerationTasks>, topic: String, ) -> Result { let t = tasks.lock().await; let progress = t.get(&topic); Ok(ClassroomProgressResponse { stage: progress.map(|p| stage_name(&p.stage).to_string()).unwrap_or_else(|| "none".to_string()), progress: progress.map(|p| p.progress).unwrap_or(0), activity: progress.map(|p| p.activity.clone()).unwrap_or_default(), items_progress: progress.and_then(|p| p.items_progress), }) } /// Cancel an active generation // @connected #[tauri::command] pub async fn classroom_cancel_generation( tasks: State<'_, GenerationTasks>, topic: String, ) -> Result<(), String> { let mut t = tasks.lock().await; t.remove(&topic); Ok(()) } /// Retrieve a generated classroom by ID // @reserved: classroom generation // @connected #[tauri::command] pub async fn classroom_get( store: State<'_, ClassroomStore>, classroom_id: String, ) -> Result { let s = store.lock().await; s.get(&classroom_id) .cloned() .ok_or_else(|| format!("Classroom '{}' not found", classroom_id)) } /// @reserved — no frontend UI yet /// List all generated classrooms (id + title only) #[tauri::command] pub async fn classroom_list( store: State<'_, ClassroomStore>, ) -> Result, String> { let s = store.lock().await; Ok(s.values().map(|c| serde_json::json!({ "id": c.id, "title": c.title, "topic": c.topic, "totalDuration": c.total_duration, "sceneCount": c.scenes.len(), })).collect()) }