File size: 16,750 Bytes
09801ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# Export Engine - Delivery Intelligence
"""
Generates executive-ready exports from chat responses.
Supports PDF, PPTX, and Email HTML formats.

No user rework required - one-click export with company branding.
"""

import io
import base64
from datetime import datetime
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
import json

# PDF generation
try:
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import letter, A4
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
    from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
    from reportlab.lib.units import inch
    PDF_AVAILABLE = True
except ImportError:
    PDF_AVAILABLE = False
    print("⚠️ ReportLab not installed - PDF export disabled")

# PPTX generation
try:
    from pptx import Presentation
    from pptx.util import Inches, Pt
    from pptx.dml.color import RGBColor
    from pptx.enum.text import PP_ALIGN
    PPTX_AVAILABLE = True
except ImportError as e:
    PPTX_AVAILABLE = False
    print(f"⚠️ python-pptx not installed - PPTX export disabled: {e}")


@dataclass
class ExportContent:
    """Content to export"""
    title: str
    answer: str
    charts: List[Dict] = None  # List of chart data/images
    tables: List[Dict] = None  # List of table data
    metadata: Dict = None      # Additional metadata
    
    def __post_init__(self):
        self.charts = self.charts or []
        self.tables = self.tables or []
        self.metadata = self.metadata or {}


@dataclass
class CompanyBranding:
    """Company branding for exports"""
    company_name: str = "Business Report"
    primary_color: str = "#3B82F6"
    secondary_color: str = "#1E40AF"
    logo_base64: Optional[str] = None
    font_family: str = "Helvetica"


class PDFExporter:
    """Generates executive PDF reports"""
    
    def __init__(self, branding: CompanyBranding = None):
        self.branding = branding or CompanyBranding()
    
    def export(self, content: ExportContent) -> bytes:
        """Generate PDF from content"""
        if not PDF_AVAILABLE:
            raise RuntimeError("ReportLab not installed. Run: pip install reportlab")
        
        buffer = io.BytesIO()
        doc = SimpleDocTemplate(buffer, pagesize=letter, topMargin=0.5*inch, bottomMargin=0.5*inch)
        
        styles = getSampleStyleSheet()
        
        # Custom styles
        title_style = ParagraphStyle(
            'CustomTitle',
            parent=styles['Heading1'],
            fontSize=18,
            textColor=colors.HexColor(self.branding.primary_color),
            spaceAfter=12,
        )
        
        body_style = ParagraphStyle(
            'CustomBody',
            parent=styles['Normal'],
            fontSize=10,
            spaceAfter=8,
            leading=14,
        )
        
        # Build story
        story = []
        
        # Header with branding
        story.append(Paragraph(self.branding.company_name, title_style))
        story.append(Paragraph(content.title, styles['Heading2']))
        story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles['Normal']))
        story.append(Spacer(1, 0.25*inch))
        
        # Main content
        # Split answer into paragraphs and format
        for paragraph in content.answer.split('\n\n'):
            if paragraph.strip():
                # Handle markdown bold
                paragraph = paragraph.replace('**', '<b>').replace('**', '</b>')
                story.append(Paragraph(paragraph.strip(), body_style))
        
        story.append(Spacer(1, 0.5*inch))
        
        # Tables
        for table_data in content.tables:
            if table_data.get('rows'):
                headers = table_data.get('headers', [])
                rows = table_data.get('rows', [])
                
                table_rows = [headers] if headers else []
                table_rows.extend(rows)
                
                if table_rows:
                    t = Table(table_rows)
                    t.setStyle(TableStyle([
                        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(self.branding.primary_color)),
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
                        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, 0), 10),
                        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
                        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
                        ('GRID', (0, 0), (-1, -1), 1, colors.black),
                    ]))
                    story.append(t)
                    story.append(Spacer(1, 0.25*inch))
        
        # Footer
        story.append(Spacer(1, 0.5*inch))
        story.append(Paragraph(
            f"© {datetime.now().year} {self.branding.company_name} | DataVision",
            styles['Normal']
        ))
        
        doc.build(story)
        buffer.seek(0)
        return buffer.getvalue()


class PPTXExporter:
    """Generates executive PowerPoint presentations"""
    
    def __init__(self, branding: CompanyBranding = None):
        self.branding = branding or CompanyBranding()
    
    def export(self, content: ExportContent) -> bytes:
        """Generate PPTX from content"""
        if not PPTX_AVAILABLE:
            raise RuntimeError("python-pptx not installed. Run: pip install python-pptx")
        
        prs = Presentation()
        prs.slide_width = Inches(13.333)
        prs.slide_height = Inches(7.5)
        
        # Title slide
        title_slide_layout = prs.slide_layouts[6]  # Blank
        slide = prs.slides.add_slide(title_slide_layout)
        
        # Add title
        txBox = slide.shapes.add_textbox(Inches(0.5), Inches(2), Inches(12), Inches(1.5))
        tf = txBox.text_frame
        p = tf.paragraphs[0]
        p.text = content.title
        p.font.size = Pt(44)
        p.font.bold = True
        p.alignment = PP_ALIGN.CENTER
        
        # Add subtitle with company name
        txBox2 = slide.shapes.add_textbox(Inches(0.5), Inches(3.5), Inches(12), Inches(1))
        tf2 = txBox2.text_frame
        p2 = tf2.paragraphs[0]
        p2.text = f"{self.branding.company_name} | {datetime.now().strftime('%B %d, %Y')}"
        p2.font.size = Pt(24)
        p2.alignment = PP_ALIGN.CENTER
        
        # Content slide with KPIs and insights
        content_layout = prs.slide_layouts[6]  # Blank
        slide2 = prs.slides.add_slide(content_layout)
        
        # Add content
        txBox3 = slide2.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(12), Inches(6.5))
        tf3 = txBox3.text_frame
        tf3.word_wrap = True
        
        # Parse content (simplified)
        paragraphs = content.answer.split('\n\n')[:5]  # Limit for slide
        for i, para in enumerate(paragraphs):
            if i == 0:
                p = tf3.paragraphs[0]
            else:
                p = tf3.add_paragraph()
            
            # Clean markdown
            para = para.replace('**', '').replace('*', '')
            p.text = para[:300]  # Limit length
            p.font.size = Pt(18 if i == 0 else 14)
            p.font.bold = (i == 0)
        
        # Add chart slides if charts are provided
        if content.charts:
            for i, chart_data in enumerate(content.charts):
                chart_slide = prs.slides.add_slide(prs.slide_layouts[6])
                
                # Chart title
                chart_title = chart_data.get('title', f'Chart {i+1}')
                title_box = chart_slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8))
                title_tf = title_box.text_frame
                title_p = title_tf.paragraphs[0]
                title_p.text = chart_title
                title_p.font.size = Pt(28)
                title_p.font.bold = True
                title_p.alignment = PP_ALIGN.CENTER
                
                # If chart has base64 image data, add it
                if chart_data.get('image_base64'):
                    try:
                        # Decode base64 image
                        img_data = chart_data['image_base64']
                        if ',' in img_data:
                            img_data = img_data.split(',')[1]  # Remove data:image/png;base64, prefix
                        
                        img_bytes = base64.b64decode(img_data)
                        img_stream = io.BytesIO(img_bytes)
                        
                        # Add image centered on slide
                        chart_slide.shapes.add_picture(
                            img_stream,
                            Inches(1.5),  # left
                            Inches(1.2),  # top
                            Inches(10),   # width
                            Inches(5.5)   # height
                        )
                    except Exception as e:
                        # If image fails, add placeholder text
                        placeholder = chart_slide.shapes.add_textbox(Inches(2), Inches(3), Inches(9), Inches(1))
                        placeholder.text_frame.paragraphs[0].text = f"Chart: {chart_title}\n(Image not available)"
                        placeholder.text_frame.paragraphs[0].font.size = Pt(18)
                else:
                    # No image - add chart description as text
                    desc_box = chart_slide.shapes.add_textbox(Inches(1), Inches(1.5), Inches(11), Inches(5))
                    desc_tf = desc_box.text_frame
                    desc_tf.word_wrap = True
                    
                    # Add chart type info
                    chart_type = chart_data.get('type', 'chart')
                    desc_p = desc_tf.paragraphs[0]
                    desc_p.text = f"Chart Type: {chart_type.replace('_', ' ').title()}"
                    desc_p.font.size = Pt(16)
                    
                    # Add data summary if available
                    if chart_data.get('data'):
                        data_info = desc_tf.add_paragraph()
                        data_p = chart_data.get('data', {})
                        if isinstance(data_p, list) and len(data_p) > 0:
                            first_trace = data_p[0]
                            x_vals = first_trace.get('x', [])[:5]
                            y_vals = first_trace.get('y', [])[:5]
                            data_info.text = f"\nSample Data:\nX: {x_vals}\nY: {y_vals}"
                            data_info.font.size = Pt(12)
        
        # Save to bytes
        buffer = io.BytesIO()
        prs.save(buffer)
        buffer.seek(0)
        return buffer.getvalue()


class EmailExporter:
    """Generates executive email HTML"""
    
    def __init__(self, branding: CompanyBranding = None):
        self.branding = branding or CompanyBranding()
    
    def export(self, content: ExportContent) -> str:
        """Generate email HTML from content"""
        
        # Convert markdown-ish to HTML
        body_html = self._markdown_to_html(content.answer)
        
        html = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {{
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }}
        .header {{
            background: linear-gradient(135deg, {self.branding.primary_color}, {self.branding.secondary_color});
            color: white;
            padding: 30px 20px;
            border-radius: 8px 8px 0 0;
            text-align: center;
        }}
        .header h1 {{
            margin: 0;
            font-size: 24px;
        }}
        .header p {{
            margin: 10px 0 0 0;
            opacity: 0.9;
            font-size: 14px;
        }}
        .content {{
            background: #ffffff;
            border: 1px solid #e0e0e0;
            border-top: none;
            padding: 30px 20px;
            border-radius: 0 0 8px 8px;
        }}
        .content h2 {{
            color: {self.branding.primary_color};
            font-size: 18px;
            margin-top: 0;
        }}
        .content p {{
            margin: 15px 0;
        }}
        .key-insight {{
            background: #f8f9fa;
            border-left: 4px solid {self.branding.primary_color};
            padding: 15px 20px;
            margin: 20px 0;
        }}
        .footer {{
            text-align: center;
            margin-top: 30px;
            padding: 20px;
            color: #666;
            font-size: 12px;
        }}
        table {{
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }}
        th, td {{
            border: 1px solid #ddd;
            padding: 10px;
            text-align: left;
        }}
        th {{
            background: {self.branding.primary_color};
            color: white;
        }}
        tr:nth-child(even) {{
            background: #f9f9f9;
        }}
    </style>
</head>
<body>
    <div class="header">
        <h1>{self.branding.company_name}</h1>
        <p>Business Intelligence Report | {datetime.now().strftime('%B %d, %Y')}</p>
    </div>
    <div class="content">
        <h2>{content.title}</h2>
        {body_html}
    </div>
    <div class="footer">
        Generated by DataVision<br>
        © {datetime.now().year} {self.branding.company_name}
    </div>
</body>
</html>
"""
        return html
    
    def _markdown_to_html(self, text: str) -> str:
        """Convert markdown-like text to HTML"""
        import re
        
        # Bold
        text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
        text = re.sub(r'__(.+?)__', r'<strong>\1</strong>', text)
        
        # Italic
        text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
        
        # Convert paragraphs
        paragraphs = text.split('\n\n')
        html_parts = []
        
        for para in paragraphs:
            para = para.strip()
            if not para:
                continue
            
            # Check if it's a key insight (starts with bold)
            if para.startswith('<strong>'):
                html_parts.append(f'<div class="key-insight">{para}</div>')
            else:
                html_parts.append(f'<p>{para}</p>')
        
        return '\n'.join(html_parts)


class ExportEngine:
    """
    Unified export engine for all formats.
    """
    
    def __init__(self, branding: CompanyBranding = None):
        self.branding = branding or CompanyBranding()
        self.pdf = PDFExporter(self.branding)
        self.pptx = PPTXExporter(self.branding)
        self.email = EmailExporter(self.branding)
    
    def export_pdf(self, content: ExportContent) -> bytes:
        """Export to PDF"""
        return self.pdf.export(content)
    
    def export_pptx(self, content: ExportContent) -> bytes:
        """Export to PowerPoint"""
        return self.pptx.export(content)
    
    def export_email(self, content: ExportContent) -> str:
        """Export to HTML email"""
        return self.email.export(content)
    
    def get_available_formats(self) -> List[str]:
        """Get list of available export formats"""
        formats = ['email']  # Email always available (pure HTML)
        if PDF_AVAILABLE:
            formats.append('pdf')
        if PPTX_AVAILABLE:
            formats.append('pptx')
        return formats


def get_export_engine(workspace_id: str = None) -> ExportEngine:
    """
    Get export engine with company branding if available.
    """
    branding = CompanyBranding()
    
    if workspace_id:
        try:
            from core.company_profile import get_company_profile, get_company_branding
            
            profile = get_company_profile(workspace_id)
            if profile:
                company_branding = get_company_branding(workspace_id)
                branding = CompanyBranding(
                    company_name=profile.company_name,
                    primary_color=company_branding.primary_color,
                    secondary_color=company_branding.secondary_color,
                    logo_base64=company_branding.logo_base64,
                    font_family=company_branding.font_family,
                )
        except Exception as e:
            print(f"⚠️ Could not load company branding: {e}")
    
    return ExportEngine(branding)