Compare commits
3 Commits
69be84e5ab
...
7ad433f260
Author | SHA1 | Date | |
---|---|---|---|
7ad433f260 | |||
18d653cc2d | |||
72cce3af0f |
300
PRESENTATION_JSON_GENERATOR_PROMPT.md
Normal file
300
PRESENTATION_JSON_GENERATOR_PROMPT.md
Normal file
@ -0,0 +1,300 @@
|
||||
# Presentation JSON Generator System Prompt
|
||||
|
||||
You are a presentation JSON generator for a slide authoring and presentation tool. Your task is to create valid JSON files that can be imported into the presentation system.
|
||||
|
||||
## JSON Structure
|
||||
|
||||
Every presentation MUST follow this exact JSON structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"id": "unique-id-here",
|
||||
"name": "Presentation Name",
|
||||
"description": "Brief description of the presentation",
|
||||
"theme": "default",
|
||||
"aspectRatio": "16:9",
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
},
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-unique-id",
|
||||
"layoutId": "layout-name",
|
||||
"content": {
|
||||
"slot-name": "content for this slot"
|
||||
},
|
||||
"notes": "Optional presenter notes",
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Field Requirements
|
||||
|
||||
### Metadata Fields:
|
||||
- **id**: Generate a unique ID using format: `pres-${timestamp}-${random}`
|
||||
- **name**: Human-readable presentation title
|
||||
- **description**: Brief description (can be empty string)
|
||||
- **theme**: Must be "default" (currently only available theme)
|
||||
- **aspectRatio**: Must be one of: "16:9", "4:3", or "16:10"
|
||||
- **createdAt/updatedAt**: ISO 8601 timestamps
|
||||
|
||||
### Slide Fields:
|
||||
- **id**: Unique slide ID, format: `slide-${index}-${timestamp}`
|
||||
- **layoutId**: MUST match one of the available layouts (see below)
|
||||
- **content**: Object mapping slot names to their content
|
||||
- **notes**: Optional presenter notes (can be omitted or empty string)
|
||||
- **order**: Sequential number starting from 0
|
||||
|
||||
## Available Layouts and Their Slots
|
||||
|
||||
### 1. title-slide
|
||||
**Purpose**: Opening slide with main title
|
||||
**Slots**:
|
||||
- `title`: Main presentation title (required)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "title-slide",
|
||||
"content": {
|
||||
"title": "Introduction to Machine Learning"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. content-slide
|
||||
**Purpose**: Standard content slide with title and body text
|
||||
**Slots**:
|
||||
- `title`: Slide title (required)
|
||||
- `content`: Main content area (multiline)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "What is Machine Learning?",
|
||||
"content": "Machine learning is a subset of artificial intelligence that enables systems to learn and improve from experience without being explicitly programmed."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 2-content-blocks
|
||||
**Purpose**: Side-by-side content comparison
|
||||
**Slots**:
|
||||
- `title`: Slide title (required)
|
||||
- `content1`: First content block (required)
|
||||
- `content2`: Second content block (required)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "2-content-blocks",
|
||||
"content": {
|
||||
"title": "Supervised vs Unsupervised Learning",
|
||||
"content1": "Supervised Learning:\n• Uses labeled data\n• Predicts outcomes\n• Examples: Classification, Regression",
|
||||
"content2": "Unsupervised Learning:\n• Uses unlabeled data\n• Finds patterns\n• Examples: Clustering, Dimensionality Reduction"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. image-slide
|
||||
**Purpose**: Display an image with title
|
||||
**Slots**:
|
||||
- `title`: Slide title (required)
|
||||
- `image`: Image data URL or path
|
||||
- `image-alt`: Alt text for accessibility (hidden field)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "image-slide",
|
||||
"content": {
|
||||
"title": "Neural Network Architecture",
|
||||
"image": "data:image/png;base64,iVBORw0KG...",
|
||||
"image-alt": "Diagram showing layers of a neural network"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. code-slide
|
||||
**Purpose**: Display code with syntax highlighting
|
||||
**Slots**:
|
||||
- `title`: Code example title (required)
|
||||
- `code`: JavaScript code (multiline)
|
||||
- `notes`: Optional explanation
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "code-slide",
|
||||
"content": {
|
||||
"title": "Simple Neural Network in JavaScript",
|
||||
"code": "class NeuralNetwork {\n constructor(inputNodes, hiddenNodes, outputNodes) {\n this.inputNodes = inputNodes;\n this.hiddenNodes = hiddenNodes;\n this.outputNodes = outputNodes;\n }\n\n train(inputs, targets) {\n // Training logic here\n }\n}",
|
||||
"notes": "This example shows a basic neural network class structure"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. markdown-slide
|
||||
**Purpose**: Rich text content with markdown formatting
|
||||
**Slots**:
|
||||
- `title`: Slide title (required)
|
||||
- `content`: Markdown content (multiline)
|
||||
|
||||
Supported Markdown:
|
||||
- Headers: `#`, `##`, `###`
|
||||
- Bold: `**text**`
|
||||
- Italic: `*text*`
|
||||
- Lists: `-` or `1.`
|
||||
- Code: `` `inline` `` or code blocks
|
||||
- Links: `[text](url)`
|
||||
- Blockquotes: `> quote`
|
||||
- Tables: GitHub-flavored markdown
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Key Concepts",
|
||||
"content": "## Machine Learning Types\n\n### Supervised Learning\n- **Classification**: Predicting categories\n- **Regression**: Predicting continuous values\n\n### Unsupervised Learning\n- **Clustering**: Grouping similar data\n- **Dimensionality Reduction**: Simplifying complex data\n\n> \"The goal is to turn data into information, and information into insight.\" - Carly Fiorina"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. diagram-slide
|
||||
**Purpose**: Display Mermaid diagrams
|
||||
**Slots**:
|
||||
- `title`: Diagram title (required)
|
||||
- `diagram`: Mermaid diagram syntax (multiline)
|
||||
- `content2`: Additional content (required)
|
||||
- `notes`: Optional explanation
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"layoutId": "diagram-slide",
|
||||
"content": {
|
||||
"title": "ML Pipeline",
|
||||
"diagram": "graph LR\n A[Data Collection] --> B[Data Preprocessing]\n B --> C[Feature Engineering]\n C --> D[Model Training]\n D --> E[Model Evaluation]\n E --> F[Deployment]",
|
||||
"content2": "Each step in the pipeline is crucial for model success",
|
||||
"notes": "This diagram shows the typical machine learning workflow"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Text Content:
|
||||
- Use `\n` for line breaks within multiline content
|
||||
- Escape quotes with backslash: `\"`
|
||||
- Keep titles concise (under 60 characters)
|
||||
- Use bullet points with `•` or `-`
|
||||
|
||||
### Markdown Content:
|
||||
- Use proper markdown syntax
|
||||
- Headers should start at `##` (title is already `#`)
|
||||
- Use backticks for code: `` `code` ``
|
||||
- Tables use pipe separators: `| Col1 | Col2 |`
|
||||
|
||||
### Code Content:
|
||||
- Properly escape special characters
|
||||
- Use `\n` for line breaks
|
||||
- Maintain proper indentation with spaces
|
||||
- Currently optimized for JavaScript syntax
|
||||
|
||||
### Diagram Content:
|
||||
- Use valid Mermaid syntax
|
||||
- Common types: graph, flowchart, sequenceDiagram
|
||||
- Direction: LR (left-right), TD (top-down), etc.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"id": "pres-1704067200000-abc123",
|
||||
"name": "Introduction to Machine Learning",
|
||||
"description": "A comprehensive overview of ML concepts and applications",
|
||||
"theme": "default",
|
||||
"aspectRatio": "16:9",
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
},
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-0-1704067200000",
|
||||
"layoutId": "title-slide",
|
||||
"content": {
|
||||
"title": "Introduction to Machine Learning"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "slide-1-1704067200001",
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "What is Machine Learning?",
|
||||
"content": "Machine learning is a subset of artificial intelligence that enables systems to learn and improve from experience without being explicitly programmed.\n\nKey characteristics:\n• Data-driven approach\n• Pattern recognition\n• Predictive capabilities\n• Continuous improvement"
|
||||
},
|
||||
"notes": "Emphasize the difference between traditional programming and ML",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "slide-2-1704067200002",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Types of Machine Learning",
|
||||
"content": "## Three Main Categories\n\n### 1. Supervised Learning\n**Training with labeled data**\n- Classification: Spam detection, image recognition\n- Regression: Price prediction, weather forecasting\n\n### 2. Unsupervised Learning\n**Finding patterns in unlabeled data**\n- Clustering: Customer segmentation\n- Dimensionality reduction: PCA, t-SNE\n\n### 3. Reinforcement Learning\n**Learning through interaction**\n- Game playing: Chess, Go\n- Robotics: Navigation, manipulation"
|
||||
},
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "slide-3-1704067200003",
|
||||
"layoutId": "code-slide",
|
||||
"content": {
|
||||
"title": "Simple Linear Regression",
|
||||
"code": "// Simple linear regression in JavaScript\nfunction linearRegression(data) {\n const n = data.length;\n let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;\n \n for (let point of data) {\n sumX += point.x;\n sumY += point.y;\n sumXY += point.x * point.y;\n sumX2 += point.x * point.x;\n }\n \n const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);\n const intercept = (sumY - slope * sumX) / n;\n \n return { slope, intercept };\n}",
|
||||
"notes": "This implements the least squares method for finding the best-fit line"
|
||||
},
|
||||
"order": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
1. **Layout IDs must exactly match**: Use only the 7 layouts listed above
|
||||
2. **Required slots cannot be empty**: Each layout has required slots marked
|
||||
3. **Use proper JSON escaping**: Escape quotes, backslashes, and special characters
|
||||
4. **Maintain slide order**: Order field should be sequential starting from 0
|
||||
5. **Generate unique IDs**: Each slide and presentation needs unique identifiers
|
||||
6. **Theme must be "default"**: Currently the only supported theme
|
||||
7. **Valid aspect ratios only**: Must be "16:9", "4:3", or "16:10"
|
||||
8. **Multiline content**: Use `\n` for line breaks in content fields
|
||||
|
||||
## File Naming
|
||||
|
||||
When saving the JSON file, use this format:
|
||||
- Replace spaces with `%20` or `-`
|
||||
- Replace special characters with URL encoding
|
||||
- Add `.json` extension
|
||||
- Example: `"My Presentation!"` becomes `My%20Presentation%21.json` or `my-presentation.json`
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before generating, verify:
|
||||
- [ ] All required metadata fields are present
|
||||
- [ ] Theme is "default"
|
||||
- [ ] Aspect ratio is valid ("16:9", "4:3", or "16:10")
|
||||
- [ ] Each slide has unique ID
|
||||
- [ ] Layout IDs match available layouts exactly
|
||||
- [ ] Required slots for each layout are filled
|
||||
- [ ] Content is properly escaped for JSON
|
||||
- [ ] Order fields are sequential from 0
|
||||
- [ ] Timestamps are valid ISO 8601 format
|
159
onlinePresentation.json
Normal file
159
onlinePresentation.json
Normal file
@ -0,0 +1,159 @@
|
||||
{
|
||||
"metadata": {
|
||||
"id": "pres-1736712000000-slideshare",
|
||||
"name": "SlideShare: Modern Web-Based Presentation Tool",
|
||||
"description": "An overview of the SlideShare presentation authoring and delivery platform",
|
||||
"theme": "default",
|
||||
"aspectRatio": "16:9",
|
||||
"createdAt": "2025-01-12T18:00:00.000Z",
|
||||
"updatedAt": "2025-01-12T18:00:00.000Z"
|
||||
},
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-0-1736712000000",
|
||||
"layoutId": "title-slide",
|
||||
"content": {
|
||||
"title": "SlideShare: Modern Presentation Authoring"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "slide-1-1736712000001",
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "What is SlideShare?",
|
||||
"content": "A powerful, browser-based presentation tool that works entirely offline.\n\n• No backend server required\n• All data stored locally in IndexedDB\n• Built with React and modern web technologies\n• Theme-based design system\n• Export and share presentations as JSON"
|
||||
},
|
||||
"notes": "Emphasize the offline-first approach and zero server dependency",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "slide-2-1736712000002",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Key Features",
|
||||
"content": "## Core Capabilities\n\n### 📝 **Intuitive Slide Authoring**\n- Visual slide editor with live preview\n- Multiple layout options per theme\n- Drag-and-drop content placement\n- Rich text and markdown support\n\n### 🎨 **Flexible Theming System**\n- Pre-built professional themes\n- Customizable layouts with HTML/CSS\n- Consistent design across presentations\n- Theme switching without content loss\n\n### 💾 **Local-First Architecture**\n- Works completely offline\n- IndexedDB for persistent storage\n- Export/import JSON presentations\n- No cloud dependency or subscription"
|
||||
},
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "slide-3-1736712000003",
|
||||
"layoutId": "2-content-blocks",
|
||||
"content": {
|
||||
"title": "Traditional vs SlideShare",
|
||||
"content1": "**Traditional Tools:**\n\n• Require installation\n• Platform-specific\n• Subscription-based\n• Cloud dependency\n• Limited customization\n• Proprietary formats",
|
||||
"content2": "**SlideShare:**\n\n• Runs in any browser\n• Cross-platform\n• Free and open\n• Works offline\n• Fully customizable\n• Open JSON format"
|
||||
},
|
||||
"notes": "Highlight the advantages of web-based, local-first approach",
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"id": "slide-4-1736712000004",
|
||||
"layoutId": "diagram-slide",
|
||||
"content": {
|
||||
"title": "Architecture Overview",
|
||||
"diagram": "graph TD\n A[Browser] --> B[React Application]\n B --> C[Presentation Editor]\n B --> D[Theme Engine]\n B --> E[Storage Layer]\n C --> F[Slide Components]\n D --> G[Layout Templates]\n D --> H[CSS Styles]\n E --> I[IndexedDB]\n E --> J[JSON Export/Import]",
|
||||
"content2": "Clean separation of concerns with modular architecture",
|
||||
"notes": "The architecture emphasizes modularity and browser-native technologies"
|
||||
},
|
||||
"order": 4
|
||||
},
|
||||
{
|
||||
"id": "slide-5-1736712000005",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Theme System Design",
|
||||
"content": "## Powerful & Flexible\n\n### **Layout Templates**\n- Simple HTML with Handlebars syntax\n- Semantic slot-based content areas\n- Multiple layouts per theme\n\n### **Styling with CSS**\n- CSS custom properties for theming\n- Responsive design built-in\n- Print-friendly styles included\n\n### **Content Slots**\n```html\n<div class=\"slot\" \n data-slot=\"title\" \n data-placeholder=\"Enter title\">\n {{title}}\n</div>\n```\n\n> Designers can create themes using only HTML and CSS - no JavaScript required!"
|
||||
},
|
||||
"order": 5
|
||||
},
|
||||
{
|
||||
"id": "slide-6-1736712000006",
|
||||
"layoutId": "code-slide",
|
||||
"content": {
|
||||
"title": "Simple Theme Creation",
|
||||
"code": "<!-- layout-quote.html -->\n<div class=\"slide layout-quote\">\n <blockquote class=\"slot quote-text\" \n data-slot=\"quote\"\n data-placeholder=\"Enter quote...\">\n {{quote}}\n </blockquote>\n \n <cite class=\"slot author-text\"\n data-slot=\"author\"\n data-placeholder=\"Author name\">\n — {{author}}\n </cite>\n</div>\n\n/* style.css */\n.layout-quote {\n justify-content: center;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n}\n\n.layout-quote blockquote {\n font-size: 2.5rem;\n font-style: italic;\n color: white;\n}",
|
||||
"notes": "Creating a new layout is as simple as HTML and CSS"
|
||||
},
|
||||
"order": 6
|
||||
},
|
||||
{
|
||||
"id": "slide-7-1736712000007",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Why It's Valuable",
|
||||
"content": "## 🚀 **For Individuals**\n- **Zero Cost**: No subscriptions or licenses\n- **Privacy First**: Your data stays on your device\n- **Work Anywhere**: No internet required after initial load\n- **Full Control**: Export and own your content\n\n## 🏢 **For Organizations**\n- **Self-Hosted**: Deploy on internal networks\n- **Customizable**: Brand-aligned themes\n- **Secure**: No data leaves the organization\n- **Standardized**: Consistent presentation format\n\n## 👩💻 **For Developers**\n- **Open Source**: Extend and customize freely\n- **Modern Stack**: React, TypeScript, Vite\n- **Clean Architecture**: Easy to understand and modify\n- **Theme API**: Create custom themes with HTML/CSS"
|
||||
},
|
||||
"order": 7
|
||||
},
|
||||
{
|
||||
"id": "slide-8-1736712000008",
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "Technical Excellence",
|
||||
"content": "Built with modern best practices:\n\n• React 19 with latest patterns\n• TypeScript for type safety\n• Vite for fast development\n• IndexedDB for robust storage\n• Semantic HTML for accessibility\n• CSS custom properties for theming\n• Responsive design principles\n• Clean, maintainable codebase"
|
||||
},
|
||||
"notes": "Emphasize the modern, professional development approach",
|
||||
"order": 8
|
||||
},
|
||||
{
|
||||
"id": "slide-9-1736712000009",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Content Types Supported",
|
||||
"content": "## Rich Content Options\n\n### 📝 **Text & Typography**\n- Plain text with formatting\n- Markdown with full syntax support\n- Custom fonts per theme\n\n### 🖼️ **Visual Elements**\n- Images with drag-and-drop upload\n- Mermaid diagrams for flowcharts\n- Code blocks with syntax highlighting\n\n### 🎯 **Layout Varieties**\n- Title slides\n- Content slides\n- Two-column layouts\n- Image showcases\n- Code demonstrations\n- Diagram presentations\n- Markdown-rich content"
|
||||
},
|
||||
"order": 9
|
||||
},
|
||||
{
|
||||
"id": "slide-10-1736712000010",
|
||||
"layoutId": "2-content-blocks",
|
||||
"content": {
|
||||
"title": "Export & Sharing",
|
||||
"content1": "**Export Options:**\n\n• JSON format for data portability\n• URL-encoded filenames\n• Complete presentation structure\n• Theme information included\n• Easy backup and restore",
|
||||
"content2": "**Sharing Features:**\n\n• Send JSON files directly\n• Import into any instance\n• Version control friendly\n• Collaborative workflows\n• Future: PDF export planned"
|
||||
},
|
||||
"order": 10
|
||||
},
|
||||
{
|
||||
"id": "slide-11-1736712000011",
|
||||
"layoutId": "diagram-slide",
|
||||
"content": {
|
||||
"title": "Development Workflow",
|
||||
"diagram": "graph LR\n A[Create Theme] --> B[Design Layouts]\n B --> C[Author Content]\n C --> D[Preview Live]\n D --> E{Happy?}\n E -->|No| C\n E -->|Yes| F[Present]\n F --> G[Export/Share]\n \n style A fill:#667eea\n style F fill:#764ba2\n style G fill:#f093fb",
|
||||
"content2": "Streamlined workflow from creation to presentation",
|
||||
"notes": "The workflow is designed to be intuitive and efficient"
|
||||
},
|
||||
"order": 11
|
||||
},
|
||||
{
|
||||
"id": "slide-12-1736712000012",
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "Future Roadmap",
|
||||
"content": "Planned enhancements:\n\n• Additional built-in themes\n• Theme marketplace/gallery\n• PDF export functionality\n• Presenter mode with notes view\n• Animation and transitions\n• Real-time collaboration\n• Plugin system for extensions\n• Mobile app for presenting"
|
||||
},
|
||||
"notes": "The project has an ambitious roadmap while maintaining simplicity",
|
||||
"order": 12
|
||||
},
|
||||
{
|
||||
"id": "slide-13-1736712000013",
|
||||
"layoutId": "markdown-slide",
|
||||
"content": {
|
||||
"title": "Get Started Today",
|
||||
"content": "## 🎯 **Quick Start**\n\n### **For Users:**\n1. Open SlideShare in your browser\n2. Create a new presentation\n3. Choose a theme\n4. Add slides and content\n5. Present or export\n\n### **For Developers:**\n```bash\n# Clone the repository\ngit clone [repo-url]\n\n# Install dependencies\nnpm install\n\n# Start development\nnpm run dev\n```\n\n### **For Designers:**\n- Create custom themes with HTML/CSS\n- No programming knowledge required\n- Use the built-in theme as a template"
|
||||
},
|
||||
"order": 13
|
||||
},
|
||||
{
|
||||
"id": "slide-14-1736712000014",
|
||||
"layoutId": "content-slide",
|
||||
"content": {
|
||||
"title": "Summary",
|
||||
"content": "SlideShare revolutionizes presentations:\n\n✅ Works entirely offline\n✅ No server or subscription required\n✅ Professional themes and layouts\n✅ Export and own your data\n✅ Open source and extensible\n✅ Modern web technologies\n✅ Privacy-focused design\n\nThe future of presentations is local-first, open, and free."
|
||||
},
|
||||
"notes": "Reinforce the key value propositions",
|
||||
"order": 14
|
||||
}
|
||||
]
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
"2-content-blocks",
|
||||
"code-slide",
|
||||
"content-slide",
|
||||
"diagram-slide",
|
||||
"image-slide",
|
||||
"markdown-slide",
|
||||
"title-slide"
|
||||
@ -14,5 +15,5 @@
|
||||
"hasMasterSlide": true
|
||||
}
|
||||
},
|
||||
"generated": "2025-08-22T02:19:52.970Z"
|
||||
"generated": "2025-09-12T13:05:24.316Z"
|
||||
}
|
16
public/themes/default/layouts/diagram-slide.html
Normal file
16
public/themes/default/layouts/diagram-slide.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="slide layout-diagram-slide">
|
||||
<h1 class="slot title-slot"
|
||||
data-slot="title"
|
||||
data-type="title"
|
||||
data-placeholder="Diagram Title"
|
||||
data-required>
|
||||
{{title}}
|
||||
</h1>
|
||||
|
||||
<div class="slot diagram-content"
|
||||
data-slot="diagram"
|
||||
data-type="diagram"
|
||||
data-placeholder="Enter Mermaid diagram syntax here..."
|
||||
data-multiline="true">{{diagram}}</div>
|
||||
|
||||
</div>
|
@ -1,4 +1,15 @@
|
||||
<div class="fade-in slide layout-image-slide">
|
||||
<style>
|
||||
.layout-image-slide .image-container {
|
||||
padding-top: 5%;
|
||||
}
|
||||
.image-container img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="I slide layout-image-slide">
|
||||
<h1 class="slot title-slot" data-slot="title" data-placeholder="Slide Title" data-required>
|
||||
{{title}}
|
||||
</h1>
|
||||
|
@ -331,6 +331,7 @@
|
||||
|
||||
/* Mermaid diagram styling */
|
||||
.mermaid-container {
|
||||
width: 100%;
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
@ -339,6 +340,10 @@
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.layout-diagram-slide > .content-slot {
|
||||
max-height: 50%;
|
||||
border: 1px solid var(--theme-secondary);
|
||||
}
|
||||
|
||||
.mermaid-diagram {
|
||||
max-width: 100%;
|
||||
@ -720,6 +725,186 @@ pre.code-content::before {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Diagram editor textarea styling */
|
||||
.field-textarea.diagram-field {
|
||||
font-family: var(--theme-font-code);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
background: #0f1419;
|
||||
color: #e6e6e6;
|
||||
border: 1px solid var(--theme-accent);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
white-space: pre;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.field-textarea.diagram-field::placeholder {
|
||||
color: #6a6a6a;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Diagram slide layout */
|
||||
.layout-diagram-slide,
|
||||
.slide-container .layout-diagram-slide {
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.layout-diagram-slide .slot[data-slot="title"] {
|
||||
font-size: clamp(1.5rem, 4vw, 2rem);
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.layout-diagram-slide .slot[data-slot="diagram"] {
|
||||
flex: 1;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-diagram-slide .slot[data-slot="notes"] {
|
||||
font-size: clamp(0.9rem, 2vw, 1rem);
|
||||
color: var(--theme-text-secondary);
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
max-height: 20vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Enhanced Mermaid diagram styling */
|
||||
.mermaid-container {
|
||||
|
||||
}
|
||||
|
||||
.mermaid-diagram svg {
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* Minimal essential styling for Mermaid text visibility */
|
||||
.mermaid-diagram svg .nodeLabel,
|
||||
.mermaid-diagram svg .edgeLabel {
|
||||
font-size: 14px !important;
|
||||
font-family: var(--theme-font-body) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Fix for foreignObject text containers */
|
||||
.mermaid-diagram svg foreignObject div {
|
||||
color: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.mermaid-diagram svg foreignObject .nodeLabel p,
|
||||
.mermaid-diagram svg foreignObject .edgeLabel p {
|
||||
color: inherit !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
||||
/* Edge label background visibility */
|
||||
.mermaid-diagram svg .edgeLabel .labelBkg {
|
||||
|
||||
}
|
||||
|
||||
.mermaid-diagram svg .edgeLabel .labelBkg p {
|
||||
|
||||
}
|
||||
.mermaid-diagram {
|
||||
|
||||
}
|
||||
|
||||
/* Responsive diagram sizing */
|
||||
@media (max-width: 768px) {
|
||||
.mermaid-container {
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.mermaid-diagram svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mermaid-diagram svg .nodeLabel,
|
||||
.mermaid-diagram svg .edgeLabel {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
.mermaid-diagram {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.diagram-error {
|
||||
color: #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ff6b6b;
|
||||
font-family: var(--theme-font-code);
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diagram-error strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ff8a8a;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Preview loading states */
|
||||
.preview-loading,
|
||||
.slide-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--theme-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 2px solid var(--theme-secondary);
|
||||
border-top: 2px solid var(--theme-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive adjustments for enhanced features */
|
||||
@media (max-width: 768px) {
|
||||
.layout-code-slide {
|
||||
|
@ -5,6 +5,7 @@ import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts';
|
||||
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
|
||||
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
|
||||
import { highlightCode } from '../../utils/codeHighlighter.ts';
|
||||
import { renderDiagram } from '../../utils/diagramProcessor.ts';
|
||||
import { loggers } from '../../utils/logger.ts';
|
||||
import './PresentationMode.css';
|
||||
import {usePresentationLoader} from "./hooks/usePresentationLoader.ts";
|
||||
@ -21,6 +22,8 @@ export const PresentationMode: React.FC = () => {
|
||||
const [currentSlideIndex, setCurrentSlideIndex] = useState(
|
||||
slideNumber ? parseInt(slideNumber, 10) - 1 : 0
|
||||
);
|
||||
const [renderedSlideContent, setRenderedSlideContent] = useState<string>('');
|
||||
const [isRenderingSlide, setIsRenderingSlide] = useState(false);
|
||||
|
||||
// Load presentation and theme
|
||||
const { presentation, theme, loading, error } = usePresentationLoader(presentationId);
|
||||
@ -52,7 +55,31 @@ export const PresentationMode: React.FC = () => {
|
||||
}
|
||||
}, [slideNumber]);
|
||||
|
||||
const renderSlideContent = (slide: SlideContent): string => {
|
||||
// Render current slide content when slide changes
|
||||
useEffect(() => {
|
||||
const renderCurrentSlide = async () => {
|
||||
if (!presentation || !theme || currentSlideIndex < 0 || currentSlideIndex >= presentation.slides.length) {
|
||||
setRenderedSlideContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRenderingSlide(true);
|
||||
try {
|
||||
const slide = presentation.slides[currentSlideIndex];
|
||||
const rendered = await renderSlideContent(slide);
|
||||
setRenderedSlideContent(rendered);
|
||||
} catch (error) {
|
||||
console.error('Failed to render slide:', error);
|
||||
setRenderedSlideContent('<div class="error">Failed to render slide</div>');
|
||||
} finally {
|
||||
setIsRenderingSlide(false);
|
||||
}
|
||||
};
|
||||
|
||||
renderCurrentSlide();
|
||||
}, [presentation, theme, currentSlideIndex]);
|
||||
|
||||
const renderSlideContent = async (slide: SlideContent): Promise<string> => {
|
||||
if (!theme) return '';
|
||||
|
||||
const layout = theme.layouts.find(l => l.id === slide.layoutId);
|
||||
@ -64,7 +91,7 @@ export const PresentationMode: React.FC = () => {
|
||||
let renderedTemplate = layout.htmlTemplate;
|
||||
|
||||
// Replace template variables with slide content
|
||||
Object.entries(slide.content).forEach(([slotId, content]) => {
|
||||
for (const [slotId, content] of Object.entries(slide.content)) {
|
||||
const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
|
||||
|
||||
// Find the corresponding slot to determine processing type
|
||||
@ -77,13 +104,16 @@ export const PresentationMode: React.FC = () => {
|
||||
// Handle code highlighting
|
||||
const language = slot.attributes?.['data-language'] || 'javascript';
|
||||
processedContent = highlightCode(content, language);
|
||||
} else if (slot?.type === 'diagram') {
|
||||
// Handle diagram rendering
|
||||
processedContent = await renderDiagram(content);
|
||||
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content))) {
|
||||
// Handle markdown processing
|
||||
processedContent = renderSlideMarkdown(content, slot?.type);
|
||||
}
|
||||
|
||||
renderedTemplate = renderedTemplate.replace(regex, processedContent);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle conditional blocks and clean up remaining variables
|
||||
renderedTemplate = renderTemplateWithSampleData(renderedTemplate, layout);
|
||||
@ -140,16 +170,21 @@ export const PresentationMode: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const currentSlide = presentation.slides[currentSlideIndex];
|
||||
const totalSlides = presentation.slides.length;
|
||||
const renderedSlideContent = renderSlideContent(currentSlide);
|
||||
|
||||
return (
|
||||
<div className="presentation-mode fullscreen">
|
||||
<div className="slide-container">
|
||||
{isRenderingSlide && (
|
||||
<div className="slide-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<span>Rendering slide...</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`slide-content ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
|
||||
dangerouslySetInnerHTML={{ __html: renderedSlideContent }}
|
||||
style={{ opacity: isRenderingSlide ? 0.5 : 1 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { Presentation } from '../../types/presentation.ts';
|
||||
import { getAllPresentations, deletePresentation } from '../../utils/presentationStorage.ts';
|
||||
import { getAllPresentations, deletePresentation, importPresentation } from '../../utils/presentationStorage.ts';
|
||||
import { loggers } from '../../utils/logger.ts';
|
||||
import { ConfirmDialog } from '../ui/ConfirmDialog.tsx';
|
||||
import { AlertDialog } from '../ui/AlertDialog.tsx';
|
||||
@ -27,6 +27,8 @@ export const PresentationsList: React.FC = () => {
|
||||
message: string;
|
||||
type?: 'error' | 'success';
|
||||
}>({ isOpen: false, message: '' });
|
||||
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadPresentations();
|
||||
@ -79,6 +81,76 @@ export const PresentationsList: React.FC = () => {
|
||||
};
|
||||
|
||||
|
||||
const handleImportPresentation = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.endsWith('.json')) {
|
||||
setAlertDialog({
|
||||
isOpen: true,
|
||||
message: 'Please select a valid JSON file.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
let jsonData;
|
||||
|
||||
try {
|
||||
jsonData = JSON.parse(text);
|
||||
} catch (parseError) {
|
||||
setAlertDialog({
|
||||
isOpen: true,
|
||||
message: 'Invalid JSON format. Please check the file.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const importedPresentation = await importPresentation(jsonData);
|
||||
await loadPresentations();
|
||||
|
||||
setAlertDialog({
|
||||
isOpen: true,
|
||||
message: `Successfully imported "${importedPresentation.metadata.name}"`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
loggers.presentation.error('Failed to import presentation', error instanceof Error ? error : new Error(String(error)));
|
||||
setAlertDialog({
|
||||
isOpen: true,
|
||||
message: error instanceof Error ? error.message : 'Failed to import presentation. Please check the file format.',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
setImporting(false);
|
||||
// Reset the input
|
||||
event.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadPresentation = (presentation: Presentation) => {
|
||||
const filename = presentation.metadata.name
|
||||
.replace(/[^a-zA-Z0-9]/g, (char) => encodeURIComponent(char))
|
||||
.toLowerCase() + '.json';
|
||||
|
||||
const dataStr = JSON.stringify(presentation, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
@ -123,6 +195,23 @@ export const PresentationsList: React.FC = () => {
|
||||
<h1>My Presentations</h1>
|
||||
<p>Manage and organize your presentation library</p>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<input
|
||||
type="file"
|
||||
id="import-presentation"
|
||||
accept=".json"
|
||||
onChange={handleImportPresentation}
|
||||
style={{ display: 'none' }}
|
||||
disabled={importing}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => document.getElementById('import-presentation')?.click()}
|
||||
disabled={importing}
|
||||
>
|
||||
{importing ? 'Importing...' : 'Import JSON'}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="list-content">
|
||||
@ -162,6 +251,14 @@ export const PresentationsList: React.FC = () => {
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon"
|
||||
onClick={() => handleDownloadPresentation(presentation)}
|
||||
title="Download as JSON"
|
||||
>
|
||||
↧
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon delete"
|
||||
@ -234,6 +331,21 @@ export const PresentationsList: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer-actions">
|
||||
<input
|
||||
type="file"
|
||||
id="import-presentation-footer"
|
||||
accept=".json"
|
||||
onChange={handleImportPresentation}
|
||||
style={{ display: 'none' }}
|
||||
disabled={importing}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => document.getElementById('import-presentation-footer')?.click()}
|
||||
disabled={importing}
|
||||
>
|
||||
{importing ? 'Importing...' : 'Import'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => navigate('/presentations/new')}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import type { Presentation } from '../../types/presentation.ts';
|
||||
import type { SlideLayout } from '../../types/theme.ts';
|
||||
import { renderTemplateWithContent } from './utils.ts';
|
||||
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
|
||||
import { ImageUploadField } from '../ui/ImageUploadField.tsx';
|
||||
import { CancelLink } from '../ui/buttons/CancelLink.tsx';
|
||||
import { ActionButton } from '../ui/buttons/ActionButton.tsx';
|
||||
import { SlidePreview } from './SlidePreview.tsx';
|
||||
|
||||
interface ContentEditorProps {
|
||||
presentation: Presentation;
|
||||
@ -45,11 +44,14 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (slot.type === 'code' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
|
||||
if (slot.type === 'code' || slot.type === 'diagram' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
|
||||
const getPlaceholder = () => {
|
||||
if (slot.type === 'code') {
|
||||
return slot.placeholder || 'Enter your JavaScript code here...';
|
||||
}
|
||||
if (slot.type === 'diagram') {
|
||||
return slot.placeholder || 'Enter Mermaid diagram syntax here...';
|
||||
}
|
||||
if (slot.type === 'markdown') {
|
||||
return slot.placeholder || 'Enter markdown content (e.g., **bold**, *italic*, - list items)';
|
||||
}
|
||||
@ -58,6 +60,7 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
|
||||
|
||||
const getRows = () => {
|
||||
if (slot.type === 'code') return 8;
|
||||
if (slot.type === 'diagram') return 10;
|
||||
if (slot.type === 'markdown') return 6;
|
||||
return 4;
|
||||
};
|
||||
@ -68,7 +71,11 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
|
||||
value={slideContent[slot.id] || ''}
|
||||
onChange={(e) => onSlotContentChange(slot.id, e.target.value)}
|
||||
placeholder={getPlaceholder()}
|
||||
className={`field-textarea ${slot.type === 'code' ? 'code-field' : slot.type === 'markdown' ? 'markdown-field' : ''}`}
|
||||
className={`field-textarea ${
|
||||
slot.type === 'code' ? 'code-field' :
|
||||
slot.type === 'diagram' ? 'diagram-field' :
|
||||
slot.type === 'markdown' ? 'markdown-field' : ''
|
||||
}`}
|
||||
rows={getRows()}
|
||||
style={slot.type === 'code' ? { fontFamily: 'var(--theme-font-code)' } : undefined}
|
||||
/>
|
||||
@ -149,31 +156,11 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-preview">
|
||||
<h3>Live Preview</h3>
|
||||
<p className="preview-description">
|
||||
Updates automatically as you type
|
||||
</p>
|
||||
<div className="preview-container">
|
||||
<div className="slide-preview-wrapper">
|
||||
<div
|
||||
className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeSlideTemplate(renderTemplateWithContent(selectedLayout, slideContent))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="preview-meta">
|
||||
<span className="layout-name">{selectedLayout.name}</span>
|
||||
<span className="aspect-ratio-info">
|
||||
{presentation.metadata.aspectRatio || '16:9'} aspect ratio
|
||||
</span>
|
||||
<span className="content-count">
|
||||
{Object.values(slideContent).filter(v => v.trim()).length} / {selectedLayout.slots.length} slots filled
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SlidePreview
|
||||
presentation={presentation}
|
||||
selectedLayout={selectedLayout}
|
||||
slideContent={slideContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
71
src/components/slide-editor/SlidePreview.tsx
Normal file
71
src/components/slide-editor/SlidePreview.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { Presentation } from '../../types/presentation.ts';
|
||||
import type { SlideLayout } from '../../types/theme.ts';
|
||||
import { renderTemplateWithContent } from './utils.ts';
|
||||
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
|
||||
|
||||
interface SlidePreviewProps {
|
||||
presentation: Presentation;
|
||||
selectedLayout: SlideLayout;
|
||||
slideContent: Record<string, string>;
|
||||
}
|
||||
|
||||
export const SlidePreview: React.FC<SlidePreviewProps> = ({
|
||||
presentation,
|
||||
selectedLayout,
|
||||
slideContent,
|
||||
}) => {
|
||||
const [previewHtml, setPreviewHtml] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updatePreview = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const rendered = await renderTemplateWithContent(selectedLayout, slideContent);
|
||||
const sanitized = sanitizeSlideTemplate(rendered);
|
||||
setPreviewHtml(sanitized);
|
||||
} catch (error) {
|
||||
console.error('Preview rendering failed:', error);
|
||||
setPreviewHtml('<div class="preview-error">Preview unavailable</div>');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
updatePreview();
|
||||
}, [selectedLayout, slideContent]);
|
||||
|
||||
return (
|
||||
<div className="content-preview">
|
||||
<h3>Live Preview</h3>
|
||||
<p className="preview-description">
|
||||
Updates automatically as you type
|
||||
</p>
|
||||
<div className="preview-container">
|
||||
<div className="slide-preview-wrapper">
|
||||
{isLoading && (
|
||||
<div className="preview-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<span>Rendering preview...</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
|
||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
||||
style={{ opacity: isLoading ? 0.5 : 1 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="preview-meta">
|
||||
<span className="layout-name">{selectedLayout.name}</span>
|
||||
<span className="aspect-ratio-info">
|
||||
{presentation.metadata.aspectRatio || '16:9'} aspect ratio
|
||||
</span>
|
||||
<span className="content-count">
|
||||
{Object.values(slideContent).filter(v => v.trim()).length} / {selectedLayout.slots.length} slots filled
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,13 +1,14 @@
|
||||
import type { SlideLayout } from '../../types/theme.ts';
|
||||
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
|
||||
import { highlightCode } from '../../utils/codeHighlighter.ts';
|
||||
import { renderDiagram } from '../../utils/diagramProcessor.ts';
|
||||
|
||||
// Helper function to render template with actual content
|
||||
export const renderTemplateWithContent = (layout: SlideLayout, content: Record<string, string>): string => {
|
||||
export const renderTemplateWithContent = async (layout: SlideLayout, content: Record<string, string>): Promise<string> => {
|
||||
let rendered = layout.htmlTemplate;
|
||||
|
||||
// Replace content placeholders
|
||||
Object.entries(content).forEach(([slotId, value]) => {
|
||||
for (const [slotId, value] of Object.entries(content)) {
|
||||
const placeholder = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
|
||||
|
||||
// Find the corresponding slot to determine processing type
|
||||
@ -20,13 +21,16 @@ export const renderTemplateWithContent = (layout: SlideLayout, content: Record<s
|
||||
// Handle code highlighting
|
||||
const language = slot.attributes?.['data-language'] || 'javascript';
|
||||
processedValue = highlightCode(value || '', language);
|
||||
} else if (slot?.type === 'diagram') {
|
||||
// Handle diagram rendering
|
||||
processedValue = await renderDiagram(value || '');
|
||||
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(value || ''))) {
|
||||
// Handle markdown processing
|
||||
processedValue = renderSlideMarkdown(value || '', slot?.type);
|
||||
}
|
||||
|
||||
rendered = rendered.replace(placeholder, processedValue);
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up any remaining placeholders
|
||||
rendered = rendered.replace(/\{\{[^}]+\}\}/g, '');
|
||||
|
170
src/utils/diagramProcessor.ts
Normal file
170
src/utils/diagramProcessor.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
/**
|
||||
* Mermaid diagram processor for slide content
|
||||
* Uses Mermaid library for diagram rendering
|
||||
*/
|
||||
|
||||
// Configure Mermaid for slide-safe diagram rendering
|
||||
mermaid.initialize({
|
||||
startOnLoad: false, // We'll manually trigger rendering
|
||||
theme: 'dark', // Match our dark theme
|
||||
themeVariables: {
|
||||
primaryColor: '#9563eb',
|
||||
primaryTextColor: '#ffffff',
|
||||
primaryBorderColor: '#94748b',
|
||||
lineColor: '#eea5e9',
|
||||
sectionBkColor: '#001112',
|
||||
altSectionBkColor: '#94748b',
|
||||
gridColor: '#94a4ab',
|
||||
secondaryColor: '#94748b',
|
||||
tertiaryColor: '#eea5e9'
|
||||
},
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontSize: 16,
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
curve: 'basis'
|
||||
},
|
||||
sequence: {
|
||||
diagramMarginX: 50,
|
||||
diagramMarginY: 10,
|
||||
actorMargin: 50,
|
||||
width: 150,
|
||||
height: 65,
|
||||
boxMargin: 10,
|
||||
boxTextMargin: 5,
|
||||
noteMargin: 10,
|
||||
messageMargin: 35
|
||||
},
|
||||
gantt: {
|
||||
titleTopMargin: 25,
|
||||
barHeight: 20,
|
||||
gridLineStartPadding: 35,
|
||||
fontSize: 11
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders Mermaid diagram from text syntax
|
||||
* @param diagramText - The Mermaid diagram syntax
|
||||
* @returns Promise resolving to HTML string with rendered diagram
|
||||
*/
|
||||
export const renderDiagram = async (diagramText: string): Promise<string> => {
|
||||
if (!diagramText || typeof diagramText !== 'string') {
|
||||
return '<div class="diagram-error">No diagram content provided</div>';
|
||||
}
|
||||
|
||||
try {
|
||||
// Clean and validate the diagram text
|
||||
const cleanedText = diagramText.trim();
|
||||
|
||||
if (!cleanedText) {
|
||||
return '<div class="diagram-error">Empty diagram content</div>';
|
||||
}
|
||||
|
||||
// Generate unique ID for this diagram
|
||||
const diagramId = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
console.log('Rendering Mermaid diagram:', cleanedText);
|
||||
|
||||
// Render the diagram directly without parse validation (parse might be causing issues)
|
||||
const { svg } = await mermaid.render(diagramId, cleanedText);
|
||||
|
||||
console.log('Mermaid SVG generated:', svg.substring(0, 100) + '...');
|
||||
|
||||
// Wrap in container with proper styling
|
||||
const result = `<div class="mermaid-container">
|
||||
<div class="mermaid-diagram" id="${diagramId}">${svg}</div>
|
||||
</div>`;
|
||||
|
||||
console.log('Final HTML result:', result.substring(0, 200) + '...');
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mermaid diagram rendering failed:', error);
|
||||
|
||||
// Return error message with the original text for debugging
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return `<div class="diagram-error">
|
||||
<strong>Diagram Error:</strong> ${errorMessage}
|
||||
<pre class="error-content">${diagramText.replace(/[<>&"']/g, (char) => {
|
||||
const escapeMap: Record<string, string> = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return escapeMap[char];
|
||||
})}</pre>
|
||||
</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if content appears to be Mermaid diagram syntax
|
||||
* Used to determine whether to process content as a diagram
|
||||
*/
|
||||
export const isDiagramContent = (content: string): boolean => {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
|
||||
// Common Mermaid diagram types
|
||||
const mermaidPatterns = [
|
||||
/^graph\s+(TD|TB|BT|RL|LR)/i, // Flowchart
|
||||
/^flowchart\s+(TD|TB|BT|RL|LR)/i, // Flowchart (newer syntax)
|
||||
/^sequenceDiagram/i, // Sequence diagram
|
||||
/^classDiagram/i, // Class diagram
|
||||
/^stateDiagram/i, // State diagram
|
||||
/^erDiagram/i, // Entity relationship diagram
|
||||
/^gantt/i, // Gantt chart
|
||||
/^pie/i, // Pie chart
|
||||
/^journey/i, // User journey
|
||||
/^gitgraph/i, // Git graph
|
||||
/^mindmap/i, // Mind map
|
||||
/^timeline/i, // Timeline
|
||||
];
|
||||
|
||||
return mermaidPatterns.some(pattern => pattern.test(trimmed));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sample diagram content for testing and previews
|
||||
*/
|
||||
export const SAMPLE_DIAGRAM_CONTENT = {
|
||||
flowchart: `graph TD
|
||||
A[Start] --> B{Is it working?}
|
||||
B -->|Yes| C[Great!]
|
||||
B -->|No| D[Debug]
|
||||
D --> B
|
||||
C --> E[End]`,
|
||||
|
||||
sequence: `sequenceDiagram
|
||||
participant A as Alice
|
||||
participant B as Bob
|
||||
participant C as Charlie
|
||||
A->>B: Hello Bob!
|
||||
B->>C: Hello Charlie!
|
||||
C->>A: Hello Alice!`,
|
||||
|
||||
pie: `pie title Project Status
|
||||
"Completed" : 65
|
||||
"In Progress" : 25
|
||||
"Planning" : 10`,
|
||||
|
||||
gantt: `gantt
|
||||
title Project Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
section Planning
|
||||
Research :done, des1, 2024-01-01, 2024-01-15
|
||||
Design :done, des2, 2024-01-10, 2024-01-25
|
||||
section Development
|
||||
Frontend :active, dev1, 2024-01-20, 2024-02-15
|
||||
Backend :dev2, 2024-02-01, 2024-02-28
|
||||
Testing :test1, 2024-02-20, 2024-03-10`
|
||||
};
|
@ -49,10 +49,27 @@ const DEFAULT_SLIDE_CONFIG: Required<SanitizeConfig> = {
|
||||
// Quotes
|
||||
'blockquote', 'cite',
|
||||
// Code elements
|
||||
'pre', 'code'
|
||||
'pre', 'code',
|
||||
// Style element for template-specific styles
|
||||
'style',
|
||||
// SVG elements for diagrams
|
||||
'svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
|
||||
'text', 'tspan', 'defs', 'marker', 'pattern', 'foreignObject', 'use', 'symbol',
|
||||
'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask'
|
||||
],
|
||||
allowedAttributes: [
|
||||
'class', 'id', 'style', 'data-*'
|
||||
'class', 'id', 'style', 'data-*', 'type',
|
||||
// SVG attributes
|
||||
'xmlns', 'viewBox', 'width', 'height', 'd', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
|
||||
'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'stroke', 'stroke-width', 'stroke-dasharray',
|
||||
'transform', 'points', 'font-family', 'font-size', 'text-anchor', 'dominant-baseline',
|
||||
// Marker attributes for arrow heads
|
||||
'markerWidth', 'markerHeight', 'orient', 'refX', 'refY', 'markerUnits',
|
||||
'marker-start', 'marker-end', 'marker-mid',
|
||||
// Additional SVG attributes needed by Mermaid
|
||||
'preserveAspectRatio', 'xmlns:xlink', 'href', 'xlink:href', 'path',
|
||||
'opacity', 'fill-opacity', 'stroke-opacity', 'stroke-linejoin', 'stroke-linecap',
|
||||
'stroke-miterlimit', 'patternUnits', 'patternTransform'
|
||||
]
|
||||
};
|
||||
|
||||
@ -78,16 +95,19 @@ export function sanitizeHtml(html: string, config: SanitizeConfig = {}): string
|
||||
FORBID_ATTR: string[];
|
||||
KEEP_CONTENT: boolean;
|
||||
ALLOW_DATA_ATTR: boolean;
|
||||
FORCE_BODY: boolean;
|
||||
} = {
|
||||
ALLOWED_TAGS: finalConfig.allowedTags,
|
||||
ALLOWED_ATTR: finalConfig.allowedAttributes,
|
||||
// Remove any scripts or dangerous content
|
||||
FORBID_TAGS: ['script', 'object', 'embed', 'base', 'link', 'meta', 'style'],
|
||||
FORBID_TAGS: ['script', 'object', 'embed', 'base', 'link', 'meta'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
|
||||
// Keep content structure but remove dangerous elements
|
||||
KEEP_CONTENT: true,
|
||||
// Allow data attributes for slots and styling
|
||||
ALLOW_DATA_ATTR: true
|
||||
ALLOW_DATA_ATTR: true,
|
||||
// Force style tags to be preserved
|
||||
FORCE_BODY: true
|
||||
};
|
||||
|
||||
// Add image support if requested
|
||||
|
@ -211,4 +211,71 @@ export const getPresentationsByTheme = async (theme: string): Promise<Presentati
|
||||
export const getPresentationMetadata = async (): Promise<PresentationMetadata[]> => {
|
||||
const presentations = await getAllPresentations();
|
||||
return presentations.map(p => p.metadata);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate imported presentation structure
|
||||
*/
|
||||
const validatePresentation = (data: any): data is Presentation => {
|
||||
// Check required metadata fields
|
||||
if (!data.metadata || typeof data.metadata !== 'object') return false;
|
||||
if (!data.metadata.id || typeof data.metadata.id !== 'string') return false;
|
||||
if (!data.metadata.name || typeof data.metadata.name !== 'string') return false;
|
||||
if (!data.metadata.theme || typeof data.metadata.theme !== 'string') return false;
|
||||
if (!data.metadata.aspectRatio || !['16:9', '4:3', '16:10'].includes(data.metadata.aspectRatio)) return false;
|
||||
|
||||
// Check slides array
|
||||
if (!Array.isArray(data.slides)) return false;
|
||||
|
||||
// Validate each slide
|
||||
for (const slide of data.slides) {
|
||||
if (!slide.id || typeof slide.id !== 'string') return false;
|
||||
if (!slide.layoutId || typeof slide.layoutId !== 'string') return false;
|
||||
if (!slide.content || typeof slide.content !== 'object') return false;
|
||||
if (typeof slide.order !== 'number') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Import a presentation from JSON
|
||||
*/
|
||||
export const importPresentation = async (jsonData: any): Promise<Presentation> => {
|
||||
// Validate the structure
|
||||
if (!validatePresentation(jsonData)) {
|
||||
throw new Error('Invalid presentation format. Please check the JSON structure.');
|
||||
}
|
||||
|
||||
const db = await initializeDB();
|
||||
|
||||
// Generate a new ID to avoid conflicts
|
||||
const importedPresentation: Presentation = {
|
||||
metadata: {
|
||||
...jsonData.metadata,
|
||||
id: generatePresentationId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
slides: jsonData.slides
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([PRESENTATIONS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(PRESENTATIONS_STORE);
|
||||
|
||||
const request = store.add(importedPresentation);
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error('Failed to import presentation'));
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(importedPresentation);
|
||||
};
|
||||
|
||||
transaction.onerror = () => {
|
||||
reject(new Error('Transaction failed while importing presentation'));
|
||||
};
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user