File size: 25,668 Bytes
7eca515
 
 
 
 
 
 
 
 
 
 
 
b95d864
7eca515
 
 
 
 
 
 
 
b95d864
7eca515
 
b95d864
 
 
7eca515
 
 
 
b95d864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eca515
 
 
 
 
 
b95d864
 
 
 
 
 
 
7eca515
b95d864
 
 
7eca515
 
b95d864
 
7eca515
b95d864
 
7eca515
b95d864
7eca515
 
 
b95d864
7eca515
b95d864
 
7eca515
 
 
 
 
 
b95d864
 
7eca515
b95d864
 
7eca515
 
 
 
 
 
 
 
 
b95d864
 
 
 
7eca515
 
 
b95d864
 
 
7eca515
 
 
b95d864
7eca515
 
b95d864
 
 
 
 
7eca515
 
 
 
b95d864
 
7eca515
 
 
 
 
 
 
 
 
b95d864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eca515
b95d864
 
7eca515
 
 
 
 
b95d864
 
 
 
 
 
7eca515
 
 
b95d864
 
 
 
 
 
 
 
 
 
7eca515
b95d864
7eca515
b95d864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eca515
b95d864
7eca515
 
b95d864
 
 
 
 
 
7eca515
 
b95d864
 
7eca515
 
 
b95d864
 
7eca515
 
 
b95d864
7eca515
 
 
b95d864
7eca515
 
b95d864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eca515
 
 
b95d864
 
 
 
 
 
7eca515
 
b95d864
 
 
 
 
 
7eca515
b95d864
 
 
 
7eca515
 
 
b95d864
7eca515
b95d864
7eca515
b95d864
7eca515
 
 
 
b95d864
 
 
 
 
7eca515
 
 
b95d864
7eca515
b95d864
 
7eca515
b95d864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eca515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
import io
import os
import re
import glob
import textwrap
from datetime import datetime
from pathlib import Path

import streamlit as st
import pandas as pd
from PIL import Image
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter # Using letter size for consistency
from reportlab.lib.utils import ImageReader
import mistune
from gtts import gTTS

# Page config
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="πŸš€")

def delete_asset(path):
    """Deletes a file asset and reruns the app."""
    try:
        os.remove(path)
        # Also remove from session state selection if it exists
        if 'selected_assets' in st.session_state and path in st.session_state.selected_assets:
            del st.session_state.selected_assets[path]
    except Exception as e:
        st.error(f"Error deleting file: {e}")
    st.rerun()

# --- New Function to Generate Combined PDF ---
def generate_combined_pdf(selected_asset_paths):
    """Generates a single PDF from selected markdown and image file paths."""
    buf = io.BytesIO()
    c = canvas.Canvas(buf)

    # --- Process Markdown Files ---
    all_plain_text = ""
    md_count = 0
    for path in selected_asset_paths:
        # Process only markdown files first
        if path.lower().endswith('.md'):
            md_count += 1
            try:
                with open(path, 'r', encoding='utf-8') as f:
                    md_text = f.read()
                # Convert Markdown to plain text using mistune (removes formatting but keeps content)
                renderer = mistune.HTMLRenderer()
                markdown = mistune.create_markdown(renderer=renderer)
                html = markdown(md_text or "")
                plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags

                if all_plain_text:
                    all_plain_text += "\n\n---\n\n" # Add a separator between combined MD files
                all_plain_text += plain_text

            except Exception as e:
                st.warning(f"Could not read or process markdown file {path}: {e}")
                # Decide if you want to continue or stop if an MD fails

    # Render combined markdown content if any was found
    if all_plain_text.strip():
        # --- Canvas Text Rendering (2 columns, 14pt font) ---
        page_w, page_h = letter # Use standard letter size (8.5 x 11 inches, approx 612 x 792 points)
        margin = 40 # Margin around the content area (points)
        gutter = 15 # Space between columns (points)
        num_columns = 2 # Fixed number of columns as requested

        # Calculate available width for text and column width
        available_text_width = page_w - 2 * margin
        col_w = (available_text_width - (num_columns - 1) * gutter) / num_columns

        font_family = "Helvetica" # A standard font available in ReportLab canvas
        font_size = 14 # Font size as requested
        c.setFont(font_family, font_size)

        # Estimate line height and character width for text wrapping
        # ReportLab units are points. Approximating char width for wrapping.
        # A common approximation for average character width is font_size * 0.6
        avg_char_width_points = font_size * 0.6
        # wrap_width is the number of characters that fit in one line of a column
        wrap_width = int(col_w / avg_char_width_points) if avg_char_width_points > 0 else 100 # Prevent division by zero

        line_height = font_size * 1.3 # Line spacing (e.g., 1.3 times font size)

        # Initialize column and vertical position
        col = 0
        x = margin + col * (col_w + gutter) # Starting x for the first column
        y = page_h - margin # Starting y from the top margin

        paragraphs = all_plain_text.split("\n")

        for paragraph in paragraphs:
            # Handle empty lines (add vertical space)
            if not paragraph.strip():
                y -= line_height / 2 # Add less space for blank lines compared to paragraphs
                # Check for page/column break after adding vertical space
                if y < margin:
                    col += 1
                    if col >= num_columns:
                        c.showPage() # Move to a new page
                        c.setFont(font_family, font_size) # Re-set font after new page
                        col = 0 # Reset to the first column
                        x = margin + col * (col_w + gutter) # Reset x position
                        y = page_h - margin # Reset y position to top margin
                    else:
                         # Move to the next column on the same page
                        x = margin + col * (col_w + gutter)
                        y = page_h - margin # Reset y position to top margin
                continue # Move to the next paragraph

            # Wrap the paragraph text into lines that fit the column width
            lines = textwrap.wrap(paragraph, wrap_width)

            for line in lines:
                # Check for page/column break before drawing the line
                if y < margin:
                    col += 1
                    if col >= num_columns:
                        c.showPage() # Move to a new page
                        c.setFont(font_family, font_size) # Re-set font after new page
                        col = 0 # Reset to the first column
                        x = margin + col * (col_w + gutter) # Reset x position
                        y = page_h - margin # Reset y position to top margin
                    else:
                        # Move to the next column on the same page
                        x = margin + col * (col_w + gutter)
                        y = page_h - margin # Reset y position to top margin

                # Draw the line
                c.drawString(x, y, line)
                # Move y position down for the next line
                y -= line_height

            # Add extra space after a paragraph (except the last one)
            if paragraph != paragraphs[-1] or lines: # Add space if it's not the very last line of the last paragraph
                 y -= line_height / 2

        # After all markdown text, ensure subsequent images start on a new page
        if all_plain_text.strip():
             c.showPage() # Start images on a fresh page

    # --- Process Image Files ---
    image_count = 0
    for path in selected_asset_paths:
        # Process image files after markdown
        if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')): # Add other image types if needed
            image_count += 1
            try:
                img = Image.open(path)
                img_w, img_h = img.size

                # Get current page size (should be letter if no text was added or after showPage)
                page_w, page_h = letter
                margin_img = 40 # Margin around the image on the page

                # Calculate available space within margins on the page
                available_w = page_w - 2 * margin_img
                available_h = page_h - 2 * margin_img

                # Calculate scaling factor to fit the image within the available space while preserving aspect ratio
                scale = min(available_w / img_w, available_h / img_h)
                draw_w = img_w * scale
                draw_h = img_h * scale

                # Calculate position to center the scaled image on the page
                pos_x = margin_img + (available_w - draw_w) / 2
                # Position from the bottom left corner
                pos_y = margin_img + (available_h - draw_h) / 2

                # Draw the image. Ensure it's on a new page.
                # If this is the first image and no text was added, it will use the initial page.
                # Otherwise, showPage() is called before drawing.
                if image_count > 1 or all_plain_text.strip():
                    c.showPage() # Start a new page for this image

                # Draw the image onto the current page
                c.drawImage(path, pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)

            except Exception as e:
                st.warning(f"Could not process image file {path}: {e}")
                continue # Continue with other selected assets

    # If no markdown or images were selected/processed
    if not all_plain_text.strip() and image_count == 0:
        page_w, page_h = letter
        c.drawString(40, page_h - 40, "No selected markdown or image files to generate PDF.")

    c.save() # Finalize the PDF
    buf.seek(0) # Rewind the buffer to the beginning
    return buf.getvalue() # Return the PDF bytes
# --- End of New Function ---


# Tabs setup
tab1, tab2 = st.tabs(["πŸ“„ PDF Composer", "πŸ§ͺ Code Interpreter"])

with tab1:
    st.header("πŸ“„ PDF Composer & Voice Generator πŸš€")

    # Sidebar settings for the original PDF composer
    # These settings (columns, font size for the *first* PDF button) are separate
    # from the settings for the combined PDF generation below.
    st.sidebar.markdown("### Original PDF Composer Settings")
    columns = st.sidebar.slider("Text columns (Original PDF)", 1, 3, 1)
    font_family = st.sidebar.selectbox("Font (Original PDF)", ["Helvetica","Times-Roman","Courier"])
    font_size = st.sidebar.slider("Font size (Original PDF)", 6, 24, 12)

    # Markdown input for the original PDF composer
    st.markdown("#### Original PDF Composer Input")
    md_file = st.file_uploader("Upload Markdown (.md) for Original PDF", type=["md"])
    if md_file:
        md_text = md_file.getvalue().decode("utf-8")
        # Use stem from uploaded file or timestamp if text area is used
        original_pdf_stem = Path(md_file.name).stem
    else:
        md_text = st.text_area("Or enter markdown text directly for Original PDF", height=200)
        original_pdf_stem = datetime.now().strftime('%Y%m%d_%H%M%S')

    # Convert Markdown to plain text for original PDF
    renderer = mistune.HTMLRenderer()
    markdown = mistune.create_markdown(renderer=renderer)
    html = markdown(md_text or "")
    original_pdf_plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags

    # Voice settings (Applies to the text entered above)
    st.markdown("#### Voice Generation from Text Input")
    languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
    voice_choice = st.selectbox("Voice Language", list(languages.keys()))
    voice_lang = languages[voice_choice]
    slow = st.checkbox("Slow Speech")

    if st.button("πŸ”Š Generate & Download Voice MP3 from Text"):
        if original_pdf_plain_text.strip():
            voice_file = f"{original_pdf_stem}.mp3"
            try:
                # Using the plain text from the text area/uploaded MD for voice
                tts = gTTS(text=original_pdf_plain_text, lang=voice_lang, slow=slow)
                tts.save(voice_file)
                st.audio(voice_file)
                with open(voice_file, 'rb') as mp3:
                    st.download_button("πŸ“₯ Download MP3", data=mp3, file_name=voice_file, mime="audio/mpeg")
            except Exception as e:
                st.error(f"Error generating voice: {e}")
        else:
            st.warning("No text to generate voice from.")

    # Image uploads and ordering for the original PDF composer
    st.markdown("#### Images for Original PDF")
    imgs = st.file_uploader("Upload Images for Original PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
    ordered_images_original_pdf = []
    if imgs:
        # Create a DataFrame for editing image order
        df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
        # Use num_rows="dynamic" for better UI, though less relevant if not adding/deleting rows
        edited = st.data_editor(df_imgs, use_container_width=True)
        # Reconstruct the ordered list of file objects based on edited order
        for _, row in edited.sort_values("order").iterrows():
            for f in imgs:
                if f.name == row['name']:
                    ordered_images_original_pdf.append(f)
                    break # Found the file object, move to the next row


    # --- Original PDF Generation Button ---
    if st.button("πŸ–‹οΈ Generate Original PDF with Markdown & Images"):
        if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
             st.warning("Please provide some text or upload images to generate the Original PDF.")
        else:
            buf = io.BytesIO()
            c = canvas.Canvas(buf)

            # Render text using original settings and logic if text is provided
            if original_pdf_plain_text.strip():
                page_w, page_h = letter
                margin = 40
                gutter = 20
                col_w = (page_w - 2*margin - (columns-1)*gutter) / columns
                c.setFont(font_family, font_size)
                line_height = font_size * 1.2
                col = 0
                x = margin
                y = page_h - margin
                # Estimate wrap width
                avg_char_width = font_size * 0.6
                wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100

                for paragraph in original_pdf_plain_text.split("\n"):
                    if not paragraph.strip(): # Handle empty lines
                         y -= line_height / 2
                         if y < margin: # Check for column/page break
                              col += 1
                              if col >= columns:
                                   c.showPage()
                                   c.setFont(font_family, font_size)
                                   col = 0
                                   x = margin + col*(col_w+gutter)
                                   y = page_h - margin
                              else:
                                   x = margin + col*(col_w+gutter)
                                   y = page_h - margin
                         continue

                    lines = textwrap.wrap(paragraph, wrap_width) if paragraph.strip() else [""]

                    for line in lines:
                        if y < margin: # Check for column/page break
                            col += 1
                            if col >= columns:
                                c.showPage()
                                c.setFont(font_family, font_size)
                                col = 0
                                x = margin + col*(col_w+gutter)
                                y = page_h - margin
                            else:
                                x = margin + col*(col_w+gutter)
                                y = page_h - margin

                        c.drawString(x, y, line)
                        y -= line_height

                    y -= line_height / 2 # Space after paragraph

                # Ensure images start on a new page if text was added
                if original_pdf_plain_text.strip():
                     c.showPage()

            # Autosize pages to each uploaded image
            image_count = 0
            for img_f in ordered_images_original_pdf:
                image_count += 1
                try:
                    img = Image.open(img_f) # img_f is a file-like object from st.file_uploader
                    w, h = img.size

                    # Start a new page for each image
                    if image_count > 1 or original_pdf_plain_text.strip():
                         c.showPage()

                    # Draw image scaled to fit a letter page within margins, centered
                    page_w, page_h = letter
                    margin_img = 40
                    available_w = page_w - 2 * margin_img
                    available_h = page_h - 2 * margin_img

                    scale = min(available_w / w, available_h / h)
                    draw_w = w * scale
                    draw_h = h * scale

                    pos_x = margin_img + (available_w - draw_w) / 2
                    pos_y = margin_img + (available_h - draw_h) / 2

                    # Use ImageReader for file-like objects
                    c.drawImage(ImageReader(img_f), pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)

                except Exception as e:
                    st.warning(f"Could not process uploaded image {img_f.name}: {e}")
                    continue

            # If nothing was generated
            if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
                 page_w, page_h = letter
                 c.drawString(40, page_h - 40, "No content to generate Original PDF.")


            c.save()
            buf.seek(0)
            pdf_name = f"{original_pdf_stem}.pdf"
            st.download_button("⬇️ Download Original PDF", data=buf, file_name=pdf_name, mime="application/pdf")

    st.markdown("---")
    st.subheader("πŸ“‚ Available Assets")
    st.markdown("Select assets below and click 'Generate Combined PDF'.")

    # Get all files and filter out unwanted ones
    all_assets = glob.glob("*.*")
    excluded_extensions = ['.py', '.ttf']
    excluded_files = ['README.md', 'index.html'] # Added index.html here

    assets = sorted([
        a for a in all_assets
        if not (a.lower().endswith(tuple(excluded_extensions)) or os.path.basename(a) in excluded_files)
    ])

    # Initialize session state for selected assets if not already done
    if 'selected_assets' not in st.session_state:
        st.session_state.selected_assets = {}

    # Ensure all current assets have an entry in session state, initialize to False if new
    # Clean up session state from assets that no longer exist
    current_asset_paths = [os.path.abspath(a) for a in assets]
    st.session_state.selected_assets = {
        k: v for k, v in st.session_state.selected_assets.items()
        if os.path.abspath(k) in current_asset_paths # Keep only existing assets
    }
    for asset_path in assets:
         if asset_path not in st.session_state.selected_assets:
              st.session_state.selected_assets[asset_path] = False


    # --- Display Assets with Checkboxes ---
    if not assets:
        st.info("No available assets found.")
    else:
        # Header row for clarity
        header_cols = st.columns([0.5, 3, 1, 1])
        header_cols[1].write("**File**")
        # header_cols[2].write("**Action**") # Optional header


        for a in assets:
            ext = a.split('.')[-1].lower()
            cols = st.columns([0.5, 3, 1, 1])

            # Checkbox in the first column, updating session state
            # Use absolute path for robust keying in case of directory changes (less likely in Streamlit sharing, but good practice)
            asset_key = os.path.abspath(a)
            st.session_state.selected_assets[a] = cols[0].checkbox("", value=st.session_state.selected_assets.get(a, False), key=f"select_asset_{asset_key}")

            # File name in the second column
            cols[1].write(a)

            # Provide download/preview based on file type in the third column
            try:
                if ext == 'pdf':
                    with open(a, 'rb') as fp:
                        cols[2].download_button("πŸ“₯", data=fp, file_name=a, mime="application/pdf", key=f"download_{a}")
                elif ext == 'mp3':
                    # Audio player takes up too much space here, just offer download
                    with open(a, 'rb') as mp3:
                         cols[2].download_button("πŸ“₯", data=mp3, file_name=a, mime="audio/mpeg", key=f"download_{a}")
                # Add more file types here if needed (e.g., images)
                elif ext in ['png', 'jpg', 'jpeg', 'gif']:
                     # Can't preview image directly in this column, offer download
                     with open(a, 'rb') as img_file:
                         cols[2].download_button("⬇️", data=img_file.read(), file_name=a, mime=f"image/{ext}", key=f"download_{a}")
                elif ext == 'md':
                    # Offer download for markdown files
                    with open(a, 'r', encoding='utf-8') as md_file:
                        cols[2].download_button("⬇️", data=md_file.read(), file_name=a, mime="text/markdown", key=f"download_{a}")
                # Handle other file types - maybe just offer download
                else:
                     with open(a, 'rb') as other_file:
                         cols[2].download_button("⬇️", data=other_file.read(), file_name=a, key=f"download_{a}") # Mime type is guessed by streamlit

                # Delete button in the fourth column
                cols[3].button("πŸ—‘οΈ", key=f"del_{a}", on_click=delete_asset, args=(a,))
            except Exception as e:
                cols[3].error(f"Error handling file {a}: {e}") # Place error in the delete column or add a separate status


    # --- Combined PDF Generation Button ---
    # Only show button if there are any assets listed
    if assets:
        if st.button("Generate Combined PDF from Selected Assets"):
            # Get the list of selected asset paths
            selected_asset_paths = [path for path, selected in st.session_state.selected_assets.items() if selected]

            if not selected_asset_paths:
                st.warning("Please select at least one asset.")
            else:
                with st.spinner("Generating combined PDF..."):
                    try:
                        # Call the new function to generate the combined PDF
                        combined_pdf_bytes = generate_combined_pdf(selected_asset_paths)

                        if combined_pdf_bytes: # Check if the function returned bytes (meaning content was added)
                            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
                            pdf_name = f"Combined_Assets_{timestamp}.pdf"
                            # Provide the generated PDF for download
                            st.download_button(
                                "⬇️ Download Combined PDF",
                                data=combined_pdf_bytes,
                                file_name=pdf_name,
                                mime="application/pdf"
                            )
                            st.success("Combined PDF generated!")
                        else:
                            st.warning("Generated PDF is empty. No valid markdown or image files were selected.")

                    except Exception as e:
                        st.error(f"An unexpected error occurred during PDF generation: {e}")


with tab2:
    st.header("πŸ§ͺ Python Code Executor & Demo")
    import io, sys
    from contextlib import redirect_stdout

    DEFAULT_CODE = '''import streamlit as st
import random

st.title("πŸ“Š Demo App")
st.markdown("Random number and color demo")

col1, col2 = st.columns(2)
with col1:
    num = st.number_input("Number:", 1, 100, 10)
    mul = st.slider("Multiplier:", 1, 10, 2)
    if st.button("Calc"):
        st.write(num * mul)
with col2:
    color = st.color_picker("Pick color","#ff0000")
    st.markdown(f'<div style="background:{color};padding:10px;">Color</div>', unsafe_allow_html=True)
''' # noqa

    def extract_python_code(md: str) -> list:
        # Find all blocks starting with ```python and ending with ```
        return re.findall(r"```python\s*(.*?)```", md, re.DOTALL)

    def execute_code(code: str) -> tuple:
        buf = io.StringIO(); local_vars = {}
        # Redirect stdout to capture print statements
        try:
            with redirect_stdout(buf):
                # Use exec to run the code. locals() and globals() are needed.
                # Passing empty dicts might limit some functionalities but provides isolation.
                exec(code, {}, local_vars)
            return buf.getvalue(), None # Return captured output
        except Exception as e:
            return None, str(e) # Return error message

    up = st.file_uploader("Upload .py or .md", type=['py', 'md'])
    # Initialize session state for code if it doesn't exist
    if 'code' not in st.session_state:
        st.session_state.code = DEFAULT_CODE

    if up:
        text = up.getvalue().decode()
        if up.type == 'text/markdown':
            codes = extract_python_code(text)
            if codes:
                 # Take the first python code block found
                st.session_state.code = codes[0].strip()
            else:
                st.warning("No Python code block found in the markdown file.")
                st.session_state.code = '' # Clear code if no block found
        else: # .py file
            st.session_state.code = text.strip()

        # Display the code after upload
        st.code(st.session_state.code, language='python')
    else:
        # Text area for code editing if no file is uploaded or after processing upload
        st.session_state.code = st.text_area("πŸ’» Code Editor", value=st.session_state.code, height=400) # Increased height

    c1, c2 = st.columns([1, 1])
    if c1.button("▢️ Run Code"):
        if st.session_state.code.strip():
            out, err = execute_code(st.session_state.code)
            if err:
                st.error(f"Execution Error:\n{err}")
            elif out:
                st.subheader("Output:")
                st.code(out)
            else:
                st.success("Executed with no standard output.")
        else:
            st.warning("No code to run.")

    if c2.button("πŸ—‘οΈ Clear Code"):
        st.session_state.code = ''
        st.rerun()