//! Presentation Analyzer //! //! Analyzes pipeline output data and recommends the best presentation type. //! //! # Strategy //! //! 1. **Structure Detection** (Fast Path, < 5ms): //! - Check for known data patterns (slides, questions, chart data) //! - Use simple heuristics for common cases //! //! 2. **LLM Analysis** (Optional, ~300ms): //! - Semantic understanding of data content //! - Better recommendations for ambiguous cases use serde_json::Value; use super::types::*; /// Presentation analyzer pub struct PresentationAnalyzer { /// Detection rules rules: Vec, } /// Detection rule for a presentation type struct DetectionRule { /// Target presentation type type_: PresentationType, /// Detection function detector: fn(&Value) -> DetectionResult, /// Priority (higher = checked first) priority: u32, } /// Result of a detection rule struct DetectionResult { /// Confidence score (0.0 - 1.0) confidence: f32, /// Reason for detection reason: String, /// Detected sub-type (e.g., "bar" for Chart) sub_type: Option, } impl PresentationAnalyzer { /// Create a new analyzer with default rules pub fn new() -> Self { let rules = vec![ // Quiz detection (high priority) DetectionRule { type_: PresentationType::Quiz, detector: detect_quiz, priority: 100, }, // Chart detection DetectionRule { type_: PresentationType::Chart, detector: detect_chart, priority: 90, }, // Slideshow detection DetectionRule { type_: PresentationType::Slideshow, detector: detect_slideshow, priority: 80, }, // Whiteboard detection DetectionRule { type_: PresentationType::Whiteboard, detector: detect_whiteboard, priority: 70, }, // Document detection (fallback, lowest priority) DetectionRule { type_: PresentationType::Document, detector: detect_document, priority: 10, }, ]; Self { rules } } /// Analyze data and recommend presentation type pub fn analyze(&self, data: &Value) -> PresentationAnalysis { // Sort rules by priority (descending) let mut sorted_rules: Vec<_> = self.rules.iter().collect(); sorted_rules.sort_by(|a, b| b.priority.cmp(&a.priority)); let mut results: Vec<(PresentationType, DetectionResult)> = Vec::new(); // Apply each detection rule for rule in sorted_rules { let result = (rule.detector)(data); if result.confidence > 0.0 { results.push((rule.type_, result)); } } // Sort by confidence results.sort_by(|a, b| { b.1.confidence.partial_cmp(&a.1.confidence).unwrap_or(std::cmp::Ordering::Equal) }); if results.is_empty() { // Fallback to document return PresentationAnalysis { recommended_type: PresentationType::Document, confidence: 0.5, reason: "无法识别数据结构,使用默认文档展示".to_string(), alternatives: vec![], structure_hints: vec!["未检测到特定结构".to_string()], sub_type: None, }; } // Build analysis result let (primary_type, primary_result) = &results[0]; let alternatives: Vec = results[1..] .iter() .filter(|(_, r)| r.confidence > 0.3) .map(|(t, r)| AlternativeType { type_: *t, confidence: r.confidence, reason: r.reason.clone(), }) .collect(); // Collect structure hints let structure_hints = collect_structure_hints(data); PresentationAnalysis { recommended_type: *primary_type, confidence: primary_result.confidence, reason: primary_result.reason.clone(), alternatives, structure_hints, sub_type: primary_result.sub_type.clone(), } } /// Quick check if data matches a specific type pub fn can_render_as(&self, data: &Value, type_: PresentationType) -> bool { for rule in &self.rules { if rule.type_ == type_ { let result = (rule.detector)(data); return result.confidence > 0.5; } } false } } impl Default for PresentationAnalyzer { fn default() -> Self { Self::new() } } // === Detection Functions === /// Detect if data is a quiz fn detect_quiz(data: &Value) -> DetectionResult { let obj = match data.as_object() { Some(o) => o, None => return DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, }, }; // Check for quiz structure if let Some(questions) = obj.get("questions").and_then(|q| q.as_array()) { if !questions.is_empty() { // Check if questions have options (choice questions) let has_options = questions.iter().any(|q| { q.get("options").and_then(|o| o.as_array()).map(|o| !o.is_empty()).unwrap_or(false) }); if has_options { return DetectionResult { confidence: 0.95, reason: "检测到问题数组,且包含选项".to_string(), sub_type: Some("choice".to_string()), }; } return DetectionResult { confidence: 0.85, reason: "检测到问题数组".to_string(), sub_type: None, }; } } // Check for quiz field if let Some(quiz) = obj.get("quiz") { if quiz.get("questions").is_some() { return DetectionResult { confidence: 0.95, reason: "包含 quiz 字段和 questions".to_string(), sub_type: None, }; } } // Check for common quiz field patterns let quiz_fields = ["questions", "answers", "score", "quiz", "exam"]; let matches: Vec<_> = quiz_fields.iter() .filter(|f| obj.contains_key(*f as &str)) .collect(); if matches.len() >= 2 { return DetectionResult { confidence: 0.6, reason: format!("包含测验相关字段: {:?}", matches), sub_type: None, }; } DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, } } /// Detect if data is a chart fn detect_chart(data: &Value) -> DetectionResult { let obj = match data.as_object() { Some(o) => o, None => return DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, }, }; // Check for explicit chart field if obj.contains_key("chart") || obj.contains_key("chartType") { let chart_type = obj.get("chartType") .and_then(|v| v.as_str()) .unwrap_or("bar"); return DetectionResult { confidence: 0.95, reason: "包含 chart/chartType 字段".to_string(), sub_type: Some(chart_type.to_string()), }; } // Check for x/y axis if obj.contains_key("xAxis") || obj.contains_key("yAxis") { return DetectionResult { confidence: 0.9, reason: "包含坐标轴定义".to_string(), sub_type: Some("line".to_string()), }; } // Check for labels + series pattern if let Some(labels) = obj.get("labels").and_then(|l| l.as_array()) { if let Some(series) = obj.get("series").and_then(|s| s.as_array()) { if !labels.is_empty() && !series.is_empty() { // Determine chart type let chart_type = if series.len() > 3 { "line" } else { "bar" }; return DetectionResult { confidence: 0.9, reason: format!("包含 labels({}) 和 series({})", labels.len(), series.len()), sub_type: Some(chart_type.to_string()), }; } } } // Check for data array with numeric values if let Some(data_arr) = obj.get("data").and_then(|d| d.as_array()) { let numeric_count = data_arr.iter() .filter(|v| v.is_number()) .count(); if numeric_count > data_arr.len() / 2 { return DetectionResult { confidence: 0.7, reason: format!("data 数组包含 {} 个数值", numeric_count), sub_type: Some("bar".to_string()), }; } } // Check for multiple data series let data_keys: Vec<_> = obj.keys() .filter(|k| k.starts_with("data") || k.ends_with("_data")) .collect(); if data_keys.len() >= 2 { return DetectionResult { confidence: 0.6, reason: format!("包含多个数据系列: {:?}", data_keys), sub_type: Some("line".to_string()), }; } DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, } } /// Detect if data is a slideshow fn detect_slideshow(data: &Value) -> DetectionResult { let obj = match data.as_object() { Some(o) => o, None => return DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, }, }; // Check for slides array if let Some(slides) = obj.get("slides").and_then(|s| s.as_array()) { if !slides.is_empty() { return DetectionResult { confidence: 0.95, reason: format!("包含 {} 张幻灯片", slides.len()), sub_type: None, }; } } // Check for sections array with title/content structure if let Some(sections) = obj.get("sections").and_then(|s| s.as_array()) { let has_slides_structure = sections.iter().all(|s| { s.get("title").is_some() && s.get("content").is_some() }); if has_slides_structure && !sections.is_empty() { return DetectionResult { confidence: 0.85, reason: format!("sections 数组包含 {} 个幻灯片结构", sections.len()), sub_type: None, }; } } // Check for scenes array (classroom style) if let Some(scenes) = obj.get("scenes").and_then(|s| s.as_array()) { if !scenes.is_empty() { return DetectionResult { confidence: 0.85, reason: format!("包含 {} 个场景", scenes.len()), sub_type: Some("classroom".to_string()), }; } } // Check for presentation-like fields let pres_fields = ["slides", "sections", "scenes", "outline", "chapters"]; let matches: Vec<_> = pres_fields.iter() .filter(|f| obj.contains_key(*f as &str)) .collect(); if matches.len() >= 2 { return DetectionResult { confidence: 0.7, reason: format!("包含演示文稿字段: {:?}", matches), sub_type: None, }; } DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, } } /// Detect if data is a whiteboard fn detect_whiteboard(data: &Value) -> DetectionResult { let obj = match data.as_object() { Some(o) => o, None => return DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, }, }; // Check for canvas/elements if obj.contains_key("canvas") || obj.contains_key("elements") { return DetectionResult { confidence: 0.9, reason: "包含 canvas/elements 字段".to_string(), sub_type: None, }; } // Check for strokes (drawing data) if obj.contains_key("strokes") { return DetectionResult { confidence: 0.95, reason: "包含 strokes 绘图数据".to_string(), sub_type: None, }; } DetectionResult { confidence: 0.0, reason: String::new(), sub_type: None, } } /// Detect if data is a document (always returns some confidence as fallback) fn detect_document(data: &Value) -> DetectionResult { let obj = match data.as_object() { Some(o) => o, None => return DetectionResult { confidence: 0.5, reason: "非对象数据,使用文档展示".to_string(), sub_type: None, }, }; // Check for markdown/text content if obj.contains_key("markdown") || obj.contains_key("content") { return DetectionResult { confidence: 0.8, reason: "包含 markdown/content 字段".to_string(), sub_type: Some("markdown".to_string()), }; } // Check for summary/report structure if obj.contains_key("summary") || obj.contains_key("report") { return DetectionResult { confidence: 0.7, reason: "包含 summary/report 字段".to_string(), sub_type: None, }; } // Default document DetectionResult { confidence: 0.5, reason: "默认文档展示".to_string(), sub_type: None, } } /// Collect structure hints from data fn collect_structure_hints(data: &Value) -> Vec { let mut hints = Vec::new(); if let Some(obj) = data.as_object() { // Check array fields for (key, value) in obj { if let Some(arr) = value.as_array() { hints.push(format!("{}: {} 项", key, arr.len())); } } // Check for common patterns if obj.contains_key("title") { hints.push("包含标题".to_string()); } if obj.contains_key("description") { hints.push("包含描述".to_string()); } if obj.contains_key("metadata") { hints.push("包含元数据".to_string()); } } hints } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_analyze_quiz() { let analyzer = PresentationAnalyzer::new(); let data = json!({ "title": "Python 测验", "questions": [ { "id": "q1", "text": "Python 是什么?", "options": [ {"id": "a", "text": "编译型语言"}, {"id": "b", "text": "解释型语言"} ] } ] }); let result = analyzer.analyze(&data); assert_eq!(result.recommended_type, PresentationType::Quiz); assert!(result.confidence > 0.8); } #[test] fn test_analyze_chart() { let analyzer = PresentationAnalyzer::new(); let data = json!({ "chartType": "bar", "title": "销售数据", "labels": ["一月", "二月", "三月"], "series": [ {"name": "销售额", "data": [100, 150, 200]} ] }); let result = analyzer.analyze(&data); assert_eq!(result.recommended_type, PresentationType::Chart); assert_eq!(result.sub_type, Some("bar".to_string())); } #[test] fn test_analyze_slideshow() { let analyzer = PresentationAnalyzer::new(); let data = json!({ "title": "课程大纲", "slides": [ {"title": "第一章", "content": "..."}, {"title": "第二章", "content": "..."} ] }); let result = analyzer.analyze(&data); assert_eq!(result.recommended_type, PresentationType::Slideshow); } #[test] fn test_analyze_document_fallback() { let analyzer = PresentationAnalyzer::new(); let data = json!({ "title": "报告", "content": "这是一段文本内容..." }); let result = analyzer.analyze(&data); assert_eq!(result.recommended_type, PresentationType::Document); } #[test] fn test_can_render_as() { let analyzer = PresentationAnalyzer::new(); let quiz_data = json!({ "questions": [{"id": "q1", "text": "问题"}] }); assert!(analyzer.can_render_as(&quiz_data, PresentationType::Quiz)); assert!(!analyzer.can_render_as(&quiz_data, PresentationType::Chart)); } }