fix(presentation): 修复 presentation 模块类型错误和语法问题
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- 创建 types.ts 定义完整的类型系统 - 重写 DocumentRenderer.tsx 修复语法错误 - 重写 QuizRenderer.tsx 修复语法错误 - 重写 PresentationContainer.tsx 添加类型守卫 - 重写 TypeSwitcher.tsx 修复类型引用 - 更新 index.ts 移除不存在的 ChartRenderer 导出 审计结果: - 类型检查: 通过 - 单元测试: 222 passed - 构建: 成功
This commit is contained in:
568
crates/zclaw-pipeline/src/presentation/analyzer.rs
Normal file
568
crates/zclaw-pipeline/src/presentation/analyzer.rs
Normal file
@@ -0,0 +1,568 @@
|
||||
//! 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 std::collections::HashMap;
|
||||
|
||||
use super::types::*;
|
||||
|
||||
/// Presentation analyzer
|
||||
pub struct PresentationAnalyzer {
|
||||
/// Detection rules
|
||||
rules: Vec<DetectionRule>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
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<AlternativeType> = 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<String> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user