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));
|
||||
}
|
||||
}
|
||||
28
crates/zclaw-pipeline/src/presentation/mod.rs
Normal file
28
crates/zclaw-pipeline/src/presentation/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Smart Presentation Layer
|
||||
//!
|
||||
//! Analyzes pipeline output and recommends the best presentation format.
|
||||
//! Supports multiple renderers: Chart, Quiz, Slideshow, Document, Whiteboard.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Pipeline Output
|
||||
//! ↓
|
||||
//! Structure Detection (fast, < 5ms)
|
||||
//! ├─→ Has slides/sections? → Slideshow
|
||||
//! ├─→ Has questions/options? → Quiz
|
||||
//! ├─→ Has chart/data arrays? → Chart
|
||||
//! └─→ Default → Document
|
||||
//! ↓
|
||||
//! LLM Analysis (optional, ~300ms)
|
||||
//! ↓
|
||||
//! Recommendation with confidence score
|
||||
//! ```
|
||||
|
||||
pub mod types;
|
||||
pub mod analyzer;
|
||||
pub mod registry;
|
||||
|
||||
pub use types::*;
|
||||
pub use analyzer::*;
|
||||
pub use registry::*;
|
||||
290
crates/zclaw-pipeline/src/presentation/registry.rs
Normal file
290
crates/zclaw-pipeline/src/presentation/registry.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
//! Presentation Registry
|
||||
//!
|
||||
//! Manages available renderers and provides lookup functionality.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::types::PresentationType;
|
||||
|
||||
/// Renderer information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RendererInfo {
|
||||
/// Renderer type
|
||||
pub type_: PresentationType,
|
||||
|
||||
/// Display name
|
||||
pub name: String,
|
||||
|
||||
/// Icon (emoji)
|
||||
pub icon: String,
|
||||
|
||||
/// Description
|
||||
pub description: String,
|
||||
|
||||
/// Supported export formats
|
||||
pub export_formats: Vec<ExportFormat>,
|
||||
|
||||
/// Is this renderer available?
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
/// Export format supported by a renderer
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExportFormat {
|
||||
/// Format ID
|
||||
pub id: String,
|
||||
|
||||
/// Display name
|
||||
pub name: String,
|
||||
|
||||
/// File extension
|
||||
pub extension: String,
|
||||
|
||||
/// MIME type
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
/// Presentation renderer registry
|
||||
pub struct PresentationRegistry {
|
||||
/// Registered renderers
|
||||
renderers: HashMap<PresentationType, RendererInfo>,
|
||||
}
|
||||
|
||||
impl PresentationRegistry {
|
||||
/// Create a new registry with default renderers
|
||||
pub fn new() -> Self {
|
||||
let mut registry = Self {
|
||||
renderers: HashMap::new(),
|
||||
};
|
||||
|
||||
// Register default renderers
|
||||
registry.register_defaults();
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
/// Register default renderers
|
||||
fn register_defaults(&mut self) {
|
||||
// Chart renderer
|
||||
self.register(RendererInfo {
|
||||
type_: PresentationType::Chart,
|
||||
name: "图表".to_string(),
|
||||
icon: "📈".to_string(),
|
||||
description: "数据可视化图表,支持折线图、柱状图、饼图等".to_string(),
|
||||
export_formats: vec![
|
||||
ExportFormat {
|
||||
id: "png".to_string(),
|
||||
name: "PNG 图片".to_string(),
|
||||
extension: "png".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "svg".to_string(),
|
||||
name: "SVG 矢量图".to_string(),
|
||||
extension: "svg".to_string(),
|
||||
mime_type: "image/svg+xml".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "json".to_string(),
|
||||
name: "JSON 数据".to_string(),
|
||||
extension: "json".to_string(),
|
||||
mime_type: "application/json".to_string(),
|
||||
},
|
||||
],
|
||||
available: true,
|
||||
});
|
||||
|
||||
// Quiz renderer
|
||||
self.register(RendererInfo {
|
||||
type_: PresentationType::Quiz,
|
||||
name: "测验".to_string(),
|
||||
icon: "✅".to_string(),
|
||||
description: "互动测验,支持选择题、判断题、填空题等".to_string(),
|
||||
export_formats: vec![
|
||||
ExportFormat {
|
||||
id: "json".to_string(),
|
||||
name: "JSON 数据".to_string(),
|
||||
extension: "json".to_string(),
|
||||
mime_type: "application/json".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "pdf".to_string(),
|
||||
name: "PDF 文档".to_string(),
|
||||
extension: "pdf".to_string(),
|
||||
mime_type: "application/pdf".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "html".to_string(),
|
||||
name: "HTML 页面".to_string(),
|
||||
extension: "html".to_string(),
|
||||
mime_type: "text/html".to_string(),
|
||||
},
|
||||
],
|
||||
available: true,
|
||||
});
|
||||
|
||||
// Slideshow renderer
|
||||
self.register(RendererInfo {
|
||||
type_: PresentationType::Slideshow,
|
||||
name: "幻灯片".to_string(),
|
||||
icon: "📊".to_string(),
|
||||
description: "演示幻灯片,支持多种布局和动画效果".to_string(),
|
||||
export_formats: vec![
|
||||
ExportFormat {
|
||||
id: "pptx".to_string(),
|
||||
name: "PowerPoint".to_string(),
|
||||
extension: "pptx".to_string(),
|
||||
mime_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "pdf".to_string(),
|
||||
name: "PDF 文档".to_string(),
|
||||
extension: "pdf".to_string(),
|
||||
mime_type: "application/pdf".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "html".to_string(),
|
||||
name: "HTML 页面".to_string(),
|
||||
extension: "html".to_string(),
|
||||
mime_type: "text/html".to_string(),
|
||||
},
|
||||
],
|
||||
available: true,
|
||||
});
|
||||
|
||||
// Document renderer
|
||||
self.register(RendererInfo {
|
||||
type_: PresentationType::Document,
|
||||
name: "文档".to_string(),
|
||||
icon: "📄".to_string(),
|
||||
description: "Markdown 文档渲染,支持代码高亮和数学公式".to_string(),
|
||||
export_formats: vec![
|
||||
ExportFormat {
|
||||
id: "md".to_string(),
|
||||
name: "Markdown".to_string(),
|
||||
extension: "md".to_string(),
|
||||
mime_type: "text/markdown".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "pdf".to_string(),
|
||||
name: "PDF 文档".to_string(),
|
||||
extension: "pdf".to_string(),
|
||||
mime_type: "application/pdf".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "html".to_string(),
|
||||
name: "HTML 页面".to_string(),
|
||||
extension: "html".to_string(),
|
||||
mime_type: "text/html".to_string(),
|
||||
},
|
||||
],
|
||||
available: true,
|
||||
});
|
||||
|
||||
// Whiteboard renderer
|
||||
self.register(RendererInfo {
|
||||
type_: PresentationType::Whiteboard,
|
||||
name: "白板".to_string(),
|
||||
icon: "🎨".to_string(),
|
||||
description: "交互式白板,支持绘图和标注".to_string(),
|
||||
export_formats: vec![
|
||||
ExportFormat {
|
||||
id: "png".to_string(),
|
||||
name: "PNG 图片".to_string(),
|
||||
extension: "png".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "svg".to_string(),
|
||||
name: "SVG 矢量图".to_string(),
|
||||
extension: "svg".to_string(),
|
||||
mime_type: "image/svg+xml".to_string(),
|
||||
},
|
||||
ExportFormat {
|
||||
id: "json".to_string(),
|
||||
name: "JSON 数据".to_string(),
|
||||
extension: "json".to_string(),
|
||||
mime_type: "application/json".to_string(),
|
||||
},
|
||||
],
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Register a renderer
|
||||
pub fn register(&mut self, info: RendererInfo) {
|
||||
self.renderers.insert(info.type_, info);
|
||||
}
|
||||
|
||||
/// Get renderer info by type
|
||||
pub fn get(&self, type_: PresentationType) -> Option<&RendererInfo> {
|
||||
self.renderers.get(&type_)
|
||||
}
|
||||
|
||||
/// Get all available renderers
|
||||
pub fn all(&self) -> Vec<&RendererInfo> {
|
||||
self.renderers.values()
|
||||
.filter(|r| r.available)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get export formats for a renderer type
|
||||
pub fn get_export_formats(&self, type_: PresentationType) -> Vec<&ExportFormat> {
|
||||
self.renderers.get(&type_)
|
||||
.map(|r| r.export_formats.iter().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Check if a renderer type is available
|
||||
pub fn is_available(&self, type_: PresentationType) -> bool {
|
||||
self.renderers.get(&type_)
|
||||
.map(|r| r.available)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PresentationRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_registry_defaults() {
|
||||
let registry = PresentationRegistry::new();
|
||||
assert!(registry.get(PresentationType::Chart).is_some());
|
||||
assert!(registry.get(PresentationType::Quiz).is_some());
|
||||
assert!(registry.get(PresentationType::Slideshow).is_some());
|
||||
assert!(registry.get(PresentationType::Document).is_some());
|
||||
assert!(registry.get(PresentationType::Whiteboard).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_export_formats() {
|
||||
let registry = PresentationRegistry::new();
|
||||
let formats = registry.get_export_formats(PresentationType::Chart);
|
||||
assert!(!formats.is_empty());
|
||||
|
||||
// Chart should support PNG
|
||||
assert!(formats.iter().any(|f| f.id == "png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_available() {
|
||||
let registry = PresentationRegistry::new();
|
||||
let available = registry.all();
|
||||
assert_eq!(available.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renderer_info() {
|
||||
let registry = PresentationRegistry::new();
|
||||
let chart = registry.get(PresentationType::Chart).unwrap();
|
||||
assert_eq!(chart.name, "图表");
|
||||
assert_eq!(chart.icon, "📈");
|
||||
}
|
||||
}
|
||||
575
crates/zclaw-pipeline/src/presentation/types.rs
Normal file
575
crates/zclaw-pipeline/src/presentation/types.rs
Normal file
@@ -0,0 +1,575 @@
|
||||
//! Presentation Types
|
||||
//!
|
||||
//! Defines presentation types, data structures, and interfaces
|
||||
//! for the smart presentation layer.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Supported presentation types
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PresentationType {
|
||||
/// Slideshow presentation (reveal.js style)
|
||||
Slideshow,
|
||||
/// Interactive quiz with questions and answers
|
||||
Quiz,
|
||||
/// Data visualization charts
|
||||
Chart,
|
||||
/// Document/Markdown rendering
|
||||
Document,
|
||||
/// Interactive whiteboard/canvas
|
||||
Whiteboard,
|
||||
/// Default fallback
|
||||
#[default]
|
||||
Auto,
|
||||
}
|
||||
|
||||
// Re-export as Quiz for consistency
|
||||
impl PresentationType {
|
||||
/// Quiz type alias
|
||||
pub const QUIZ: Self = Self::Quiz;
|
||||
}
|
||||
|
||||
impl PresentationType {
|
||||
/// Get display name
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Slideshow => "幻灯片",
|
||||
Self::Quiz => "测验",
|
||||
Self::Chart => "图表",
|
||||
Self::Document => "文档",
|
||||
Self::Whiteboard => "白板",
|
||||
Self::Auto => "自动",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get icon emoji
|
||||
pub fn icon(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Slideshow => "📊",
|
||||
Self::Quiz => "✅",
|
||||
Self::Chart => "📈",
|
||||
Self::Document => "📄",
|
||||
Self::Whiteboard => "🎨",
|
||||
Self::Auto => "🔄",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all available types (excluding Auto)
|
||||
pub fn all() -> &'static [PresentationType] {
|
||||
&[
|
||||
Self::Slideshow,
|
||||
Self::Quiz,
|
||||
Self::Chart,
|
||||
Self::Document,
|
||||
Self::Whiteboard,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Chart sub-types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ChartType {
|
||||
/// Line chart
|
||||
Line,
|
||||
/// Bar chart
|
||||
Bar,
|
||||
/// Pie chart
|
||||
Pie,
|
||||
/// Scatter plot
|
||||
Scatter,
|
||||
/// Area chart
|
||||
Area,
|
||||
/// Radar chart
|
||||
Radar,
|
||||
/// Heatmap
|
||||
Heatmap,
|
||||
}
|
||||
|
||||
/// Quiz question types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum QuestionType {
|
||||
/// Single choice
|
||||
SingleChoice,
|
||||
/// Multiple choice
|
||||
MultipleChoice,
|
||||
/// True/False
|
||||
TrueFalse,
|
||||
/// Fill in the blank
|
||||
FillBlank,
|
||||
/// Short answer
|
||||
ShortAnswer,
|
||||
/// Matching
|
||||
Matching,
|
||||
/// Ordering
|
||||
Ordering,
|
||||
}
|
||||
|
||||
/// Presentation analysis result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PresentationAnalysis {
|
||||
/// Recommended presentation type
|
||||
pub recommended_type: PresentationType,
|
||||
|
||||
/// Confidence score (0.0 - 1.0)
|
||||
pub confidence: f32,
|
||||
|
||||
/// Reason for recommendation
|
||||
pub reason: String,
|
||||
|
||||
/// Alternative types that could work
|
||||
pub alternatives: Vec<AlternativeType>,
|
||||
|
||||
/// Detected data structure hints
|
||||
pub structure_hints: Vec<String>,
|
||||
|
||||
/// Specific sub-type recommendation (e.g., "line" for Chart)
|
||||
pub sub_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Alternative presentation type with confidence
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlternativeType {
|
||||
pub type_: PresentationType,
|
||||
pub confidence: f32,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// Chart data structure for ChartRenderer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChartData {
|
||||
/// Chart type
|
||||
pub chart_type: ChartType,
|
||||
|
||||
/// Chart title
|
||||
pub title: Option<String>,
|
||||
|
||||
/// X-axis labels
|
||||
pub labels: Vec<String>,
|
||||
|
||||
/// Data series
|
||||
pub series: Vec<ChartSeries>,
|
||||
|
||||
/// X-axis configuration
|
||||
pub x_axis: Option<AxisConfig>,
|
||||
|
||||
/// Y-axis configuration
|
||||
pub y_axis: Option<AxisConfig>,
|
||||
|
||||
/// Legend configuration
|
||||
pub legend: Option<LegendConfig>,
|
||||
|
||||
/// Additional options
|
||||
#[serde(default)]
|
||||
pub options: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Chart series data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChartSeries {
|
||||
/// Series name
|
||||
pub name: String,
|
||||
|
||||
/// Data values
|
||||
pub data: Vec<f64>,
|
||||
|
||||
/// Series color
|
||||
pub color: Option<String>,
|
||||
|
||||
/// Series type (for mixed charts)
|
||||
pub series_type: Option<ChartType>,
|
||||
}
|
||||
|
||||
/// Axis configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AxisConfig {
|
||||
/// Axis label
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Min value
|
||||
pub min: Option<f64>,
|
||||
|
||||
/// Max value
|
||||
pub max: Option<f64>,
|
||||
|
||||
/// Show grid lines
|
||||
#[serde(default = "default_true")]
|
||||
pub show_grid: bool,
|
||||
}
|
||||
|
||||
/// Legend configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LegendConfig {
|
||||
/// Show legend
|
||||
#[serde(default = "default_true")]
|
||||
pub show: bool,
|
||||
|
||||
/// Legend position: top, bottom, left, right
|
||||
pub position: Option<String>,
|
||||
}
|
||||
|
||||
/// Quiz data structure for QuizRenderer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QuizData {
|
||||
/// Quiz title
|
||||
pub title: Option<String>,
|
||||
|
||||
/// Quiz description
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Questions
|
||||
pub questions: Vec<QuizQuestion>,
|
||||
|
||||
/// Time limit in seconds (optional)
|
||||
pub time_limit: Option<u32>,
|
||||
|
||||
/// Show correct answers after submission
|
||||
#[serde(default = "default_true")]
|
||||
pub show_answers: bool,
|
||||
|
||||
/// Allow retry
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_retry: bool,
|
||||
|
||||
/// Passing score percentage (0-100)
|
||||
pub passing_score: Option<u32>,
|
||||
}
|
||||
|
||||
/// Quiz question
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QuizQuestion {
|
||||
/// Question ID
|
||||
pub id: String,
|
||||
|
||||
/// Question text
|
||||
pub text: String,
|
||||
|
||||
/// Question type
|
||||
#[serde(rename = "type")]
|
||||
pub question_type: QuestionType,
|
||||
|
||||
/// Options for choice questions
|
||||
#[serde(default)]
|
||||
pub options: Vec<QuestionOption>,
|
||||
|
||||
/// Correct answer(s)
|
||||
/// - Single choice: single index or value
|
||||
/// - Multiple choice: array of indices
|
||||
/// - Fill blank: the expected text
|
||||
pub correct_answer: serde_json::Value,
|
||||
|
||||
/// Explanation shown after answering
|
||||
pub explanation: Option<String>,
|
||||
|
||||
/// Points for this question
|
||||
#[serde(default = "default_points")]
|
||||
pub points: u32,
|
||||
|
||||
/// Image URL (optional)
|
||||
pub image: Option<String>,
|
||||
|
||||
/// Hint text
|
||||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
fn default_points() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// Question option for choice questions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QuestionOption {
|
||||
/// Option ID (a, b, c, d or 0, 1, 2, 3)
|
||||
pub id: String,
|
||||
|
||||
/// Option text
|
||||
pub text: String,
|
||||
|
||||
/// Optional image
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
/// Slideshow data structure for SlideshowRenderer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SlideshowData {
|
||||
/// Presentation title
|
||||
pub title: String,
|
||||
|
||||
/// Presentation subtitle
|
||||
pub subtitle: Option<String>,
|
||||
|
||||
/// Author
|
||||
pub author: Option<String>,
|
||||
|
||||
/// Slides
|
||||
pub slides: Vec<Slide>,
|
||||
|
||||
/// Theme
|
||||
pub theme: Option<SlideshowTheme>,
|
||||
|
||||
/// Transition effect
|
||||
pub transition: Option<String>,
|
||||
}
|
||||
|
||||
/// Single slide
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Slide {
|
||||
/// Slide ID
|
||||
pub id: String,
|
||||
|
||||
/// Slide title
|
||||
pub title: Option<String>,
|
||||
|
||||
/// Slide content
|
||||
pub content: SlideContent,
|
||||
|
||||
/// Speaker notes
|
||||
pub notes: Option<String>,
|
||||
|
||||
/// Background color or image
|
||||
pub background: Option<String>,
|
||||
|
||||
/// Transition for this slide
|
||||
pub transition: Option<String>,
|
||||
}
|
||||
|
||||
/// Slide content types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SlideContent {
|
||||
/// Title slide
|
||||
Title {
|
||||
heading: String,
|
||||
subheading: Option<String>,
|
||||
},
|
||||
|
||||
/// Bullet points
|
||||
Bullets {
|
||||
items: Vec<String>,
|
||||
},
|
||||
|
||||
/// Two columns
|
||||
TwoColumns {
|
||||
left: Vec<String>,
|
||||
right: Vec<String>,
|
||||
},
|
||||
|
||||
/// Image with caption
|
||||
Image {
|
||||
url: String,
|
||||
caption: Option<String>,
|
||||
alt: Option<String>,
|
||||
},
|
||||
|
||||
/// Code block
|
||||
Code {
|
||||
language: String,
|
||||
code: String,
|
||||
filename: Option<String>,
|
||||
},
|
||||
|
||||
/// Quote
|
||||
Quote {
|
||||
text: String,
|
||||
author: Option<String>,
|
||||
},
|
||||
|
||||
/// Table
|
||||
Table {
|
||||
headers: Vec<String>,
|
||||
rows: Vec<Vec<String>>,
|
||||
},
|
||||
|
||||
/// Chart (embedded)
|
||||
Chart {
|
||||
chart_data: ChartData,
|
||||
},
|
||||
|
||||
/// Quiz (embedded)
|
||||
Quiz {
|
||||
quiz_data: QuizData,
|
||||
},
|
||||
|
||||
/// Custom HTML/Markdown
|
||||
Custom {
|
||||
html: Option<String>,
|
||||
markdown: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Slideshow theme
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SlideshowTheme {
|
||||
/// Primary color
|
||||
pub primary_color: Option<String>,
|
||||
|
||||
/// Secondary color
|
||||
pub secondary_color: Option<String>,
|
||||
|
||||
/// Background color
|
||||
pub background_color: Option<String>,
|
||||
|
||||
/// Text color
|
||||
pub text_color: Option<String>,
|
||||
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
|
||||
/// Code font
|
||||
pub code_font: Option<String>,
|
||||
}
|
||||
|
||||
/// Whiteboard data structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WhiteboardData {
|
||||
/// Canvas width
|
||||
pub width: u32,
|
||||
|
||||
/// Canvas height
|
||||
pub height: u32,
|
||||
|
||||
/// Background color
|
||||
pub background: Option<String>,
|
||||
|
||||
/// Drawing elements
|
||||
pub elements: Vec<WhiteboardElement>,
|
||||
}
|
||||
|
||||
/// Whiteboard element
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum WhiteboardElement {
|
||||
/// Path/stroke
|
||||
Path {
|
||||
id: String,
|
||||
points: Vec<Point>,
|
||||
color: String,
|
||||
width: f32,
|
||||
opacity: f32,
|
||||
},
|
||||
|
||||
/// Text
|
||||
Text {
|
||||
id: String,
|
||||
text: String,
|
||||
position: Point,
|
||||
font_size: u32,
|
||||
color: String,
|
||||
},
|
||||
|
||||
/// Rectangle
|
||||
Rectangle {
|
||||
id: String,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
fill: Option<String>,
|
||||
stroke: Option<String>,
|
||||
stroke_width: f32,
|
||||
},
|
||||
|
||||
/// Circle/Ellipse
|
||||
Circle {
|
||||
id: String,
|
||||
cx: f32,
|
||||
cy: f32,
|
||||
radius: f32,
|
||||
fill: Option<String>,
|
||||
stroke: Option<String>,
|
||||
stroke_width: f32,
|
||||
},
|
||||
|
||||
/// Image
|
||||
Image {
|
||||
id: String,
|
||||
url: String,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
},
|
||||
}
|
||||
|
||||
/// 2D Point
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Point {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_presentation_type_display() {
|
||||
assert_eq!(PresentationType::Slideshow.display_name(), "幻灯片");
|
||||
assert_eq!(PresentationType::Chart.display_name(), "图表");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_presentation_type_icon() {
|
||||
assert_eq!(PresentationType::Quiz.icon(), "✅");
|
||||
assert_eq!(PresentationType::Document.icon(), "📄");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quiz_data_deserialize() {
|
||||
let json = r#"{
|
||||
"title": "Python 基础测验",
|
||||
"questions": [
|
||||
{
|
||||
"id": "q1",
|
||||
"text": "Python 是什么类型的语言?",
|
||||
"type": "singleChoice",
|
||||
"options": [
|
||||
{"id": "a", "text": "编译型"},
|
||||
{"id": "b", "text": "解释型"}
|
||||
],
|
||||
"correctAnswer": "b"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let quiz: QuizData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(quiz.questions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chart_data_deserialize() {
|
||||
let json = r#"{
|
||||
"chartType": "bar",
|
||||
"title": "月度销售",
|
||||
"labels": ["一月", "二月", "三月"],
|
||||
"series": [
|
||||
{"name": "销售额", "data": [100, 150, 200]}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let chart: ChartData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(chart.labels.len(), 3);
|
||||
assert_eq!(chart.series[0].data.len(), 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user