fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
286
desktop/src-tauri/src/classroom_commands/generate.rs
Normal file
286
desktop/src-tauri/src/classroom_commands/generate.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
//! 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<String>,
|
||||
pub style: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub target_duration_minutes: Option<u32>,
|
||||
pub scene_count: Option<usize>,
|
||||
pub custom_instructions: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[tauri::command]
|
||||
pub async fn classroom_generate(
|
||||
app: AppHandle,
|
||||
store: State<'_, ClassroomStore>,
|
||||
tasks: State<'_, GenerationTasks>,
|
||||
kernel_state: State<'_, KernelState>,
|
||||
request: ClassroomGenerateRequest,
|
||||
) -> Result<ClassroomGenerateResponse, String> {
|
||||
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 = {
|
||||
let ks = kernel_state.lock().await;
|
||||
if let Some(kernel) = ks.as_ref() {
|
||||
GenerationPipeline::with_driver(kernel.driver())
|
||||
} else {
|
||||
GenerationPipeline::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: check if cancelled
|
||||
let is_cancelled = || {
|
||||
let t = tasks.blocking_lock();
|
||||
!t.contains_key(&topic_clone)
|
||||
};
|
||||
|
||||
// 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::<Vec<_>>();
|
||||
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(),
|
||||
custom: serde_json::Map::new(),
|
||||
},
|
||||
};
|
||||
|
||||
// Store classroom
|
||||
{
|
||||
let mut s = store.lock().await;
|
||||
s.insert(classroom_id.clone(), classroom);
|
||||
}
|
||||
|
||||
// Clear generation task
|
||||
{
|
||||
let mut t = tasks.lock().await;
|
||||
t.remove(&topic_clone);
|
||||
}
|
||||
|
||||
// Emit completion
|
||||
emit_progress("complete", 100, "课堂生成完成");
|
||||
|
||||
Ok(ClassroomGenerateResponse {
|
||||
classroom_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current generation progress for a topic
|
||||
#[tauri::command]
|
||||
pub async fn classroom_generation_progress(
|
||||
tasks: State<'_, GenerationTasks>,
|
||||
topic: String,
|
||||
) -> Result<ClassroomProgressResponse, String> {
|
||||
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
|
||||
#[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
|
||||
#[tauri::command]
|
||||
pub async fn classroom_get(
|
||||
store: State<'_, ClassroomStore>,
|
||||
classroom_id: String,
|
||||
) -> Result<Classroom, String> {
|
||||
let s = store.lock().await;
|
||||
s.get(&classroom_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Classroom '{}' not found", classroom_id))
|
||||
}
|
||||
|
||||
/// List all generated classrooms (id + title only)
|
||||
#[tauri::command]
|
||||
pub async fn classroom_list(
|
||||
store: State<'_, ClassroomStore>,
|
||||
) -> Result<Vec<serde_json::Value>, 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())
|
||||
}
|
||||
Reference in New Issue
Block a user