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

- 创建 types.ts 定义完整的类型系统
- 重写 DocumentRenderer.tsx 修复语法错误
- 重写 QuizRenderer.tsx 修复语法错误
- 重写 PresentationContainer.tsx 添加类型守卫
- 重写 TypeSwitcher.tsx 修复类型引用
- 更新 index.ts 移除不存在的 ChartRenderer 导出

审计结果:
- 类型检查: 通过
- 单元测试: 222 passed
- 构建: 成功
This commit is contained in:
iven
2026-03-26 17:19:28 +08:00
parent d0c6319fc1
commit b7f3d94950
71 changed files with 15896 additions and 1133 deletions

View 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));
}
}

View 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::*;

View 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, "📈");
}
}

View 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);
}
}