broadfield-dev commited on
Commit
f6e05b2
·
verified ·
1 Parent(s): fb80aa3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -124
app.py CHANGED
@@ -17,12 +17,10 @@ os.makedirs(TEMP_DIR, exist_ok=True)
17
 
18
  def parse_repo2markdown(text):
19
  components = []
20
- # Extract File Structure specifically
21
  struct_match = re.search(r'## File Structure\n([\s\S]*?)(?=\n### File:|\Z)', text)
22
  if struct_match:
23
  components.append({'type': 'structure', 'filename': 'File Structure', 'content': struct_match.group(1).strip()})
24
 
25
- # Pattern to capture file sections
26
  pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
27
  for match in pattern.finditer(text):
28
  filename = match.group(1).strip()
@@ -45,7 +43,7 @@ def parse_changelog(text):
45
  components = []
46
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
47
  if parts[0].strip():
48
- components.append({'type': 'intro', 'filename': 'Changelog Header', 'content': parts[0].strip()})
49
  for i in range(1, len(parts), 2):
50
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
51
  return components
@@ -60,11 +58,16 @@ def parse_agent_action(text):
60
  components.append({'type': 'file', 'filename': match.group(1).strip(), 'content': match.group(2).strip()})
61
  return components
62
 
63
- def build_full_html(markdown_text, styles, include_fontawesome):
64
  wrapper_id = "#output-wrapper"
65
- font_family = styles.get('font_family', "'Inter', sans-serif")
66
- google_font_name = font_family.split(',')[0].strip("'\"")
67
- google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={google_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
 
 
 
 
 
68
 
69
  highlight_theme = styles.get('highlight_theme', 'monokai')
70
  pygments_css = ""
@@ -90,8 +93,7 @@ def build_full_html(markdown_text, styles, include_fontawesome):
90
  {styles.get('custom_css', '')}
91
  """
92
  html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables', 'codehilite', 'nl2br'])
93
- fa = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">' if include_fontawesome else ""
94
- return f"<!DOCTYPE html><html><head><meta charset='UTF-8'>{google_font_link}{fa}<style>{scoped_css}</style></head><body><div id='output-wrapper'>{html_content}</div></body></html>"
95
 
96
  @app.route('/parse', methods=['POST'])
97
  def parse_endpoint():
@@ -115,18 +117,28 @@ def parse_endpoint():
115
  def convert_endpoint():
116
  data = request.json
117
  try:
118
- full_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), data.get('include_fontawesome', True))
119
- options = {"quiet": "", "encoding": "UTF-8", "width": 1024, "disable-smart-width": ""}
 
 
 
 
 
 
 
 
 
 
120
 
121
  if data.get('download', False):
122
  if data.get('download_type') == 'html':
123
- return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
124
- png_bytes = imgkit.from_string(full_html, False, options=options)
125
  return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
126
 
127
- png_bytes = imgkit.from_string(full_html, False, options=options)
128
  return jsonify({
129
- 'preview_html': full_html,
130
  'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')
131
  })
132
  except Exception as e:
@@ -144,26 +156,23 @@ def index():
144
  :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; --primary: #5a32a3; }
145
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
146
  body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: var(--bg); color: var(--text); line-height: 1.4; transition: background 0.3s; }
147
- h1 { text-align: center; }
148
  fieldset { border: 1px solid var(--border); background: var(--card); padding: 20px; margin-bottom: 25px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
149
  legend { font-weight: bold; padding: 0 10px; color: var(--primary); }
150
  textarea { width: 100%; border-radius: 6px; padding: 12px; border: 1px solid var(--border); background: var(--card); color: var(--text); font-family: 'Fira Code', monospace; box-sizing: border-box; }
151
  .format-banner { background: var(--primary); color: white; padding: 6px 16px; border-radius: 20px; font-size: 13px; display: inline-block; margin-bottom: 15px; font-weight: bold; }
152
  .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
153
  .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
154
- .comp-card { background: var(--card); border: 1px solid var(--border); padding: 12px; border-radius: 8px; position: relative; }
155
  .comp-card textarea { height: 60px; font-size: 11px; margin-top: 8px; opacity: 0.8; pointer-events: none; resize: none; }
156
- .controls { display: flex; flex-direction: column; gap: 8px; }
157
  .action-bar { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
158
  button { padding: 12px 24px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; transition: opacity 0.2s; }
159
  .btn-primary { background: var(--primary); color: #fff; flex: 1; min-width: 200px; }
160
  .btn-secondary { background: #333; color: #fff; border: 1px solid #555; }
161
- .btn-download { background: #28a745; color: white; }
162
  .preview-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
163
- .preview-box { background: #fff; border: 1px solid #ddd; padding: 0; border-radius: 8px; overflow: hidden; height: 600px; display: flex; flex-direction: column; }
164
- .preview-header { background: #eee; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; color: #333; border-bottom: 1px solid #ddd; }
165
  .preview-content { flex: 1; overflow: auto; padding: 15px; }
166
- @media (max-width: 900px) { .preview-section { grid-template-columns: 1fr; } }
167
  </style>
168
  </head>
169
  <body>
@@ -174,7 +183,7 @@ def index():
174
 
175
  <fieldset>
176
  <legend>1. Input Source</legend>
177
- <textarea id="md-input" rows="10" placeholder="Paste your Markdown, Repo2Markdown, or Agent Action text here..."></textarea>
178
  <div class="action-bar"><button onclick="analyze()" class="btn-primary" id="load-btn">Analyze & Extract Components</button></div>
179
  </fieldset>
180
 
@@ -189,110 +198,75 @@ def index():
189
  <fieldset>
190
  <legend>3. Visual Styling</legend>
191
  <div class="style-grid">
192
- <div class="controls"><label>Font Family</label>
193
  <select id="f_family">
194
- <option value="'Inter', sans-serif">Inter (Modern)</option>
195
- <option value="'Fira Code', monospace">Fira Code (Monospace)</option>
196
- <option value="'Playfair Display', serif">Playfair (Serif)</option>
197
- <option value="'Roboto', sans-serif">Roboto</option>
198
  </select>
199
  </div>
200
- <div class="controls"><label>Font Size (px)</label><input type="number" id="f_size" value="16"></div>
201
- <div class="controls"><label>Line Height</label><input type="number" id="l_height" value="1.6" step="0.1"></div>
202
- <div class="controls"><label>Text Color</label><input type="color" id="t_color" value="#333333"></div>
203
- <div class="controls"><label>Background</label><input type="color" id="b_color" value="#ffffff"></div>
204
- <div class="controls"><label>Syntax Theme</label>
205
  <select id="h_theme">
206
  {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
207
  </select>
208
  </div>
209
  </div>
210
- <div style="margin-top:20px;">
211
  <label><b>Custom CSS Overrides</b></label>
212
- <textarea id="c_css" rows="3" placeholder="#output-wrapper h1 { color: purple; }"></textarea>
213
  </div>
214
  </fieldset>
215
 
216
- <div class="action-bar">
217
- <button onclick="process('preview')" class="btn-primary" id="gen-btn">GENERATE PREVIEW</button>
218
- </div>
219
 
220
  <div id="preview-area" style="display:none;">
221
  <div class="preview-section">
222
  <div class="preview-box">
223
- <div class="preview-header">
224
- <span>HTML Preview</span>
225
- <button class="btn-download" onclick="process('download', 'html')">Download .html</button>
226
- </div>
227
  <div id="html-prev" class="preview-content"></div>
228
  </div>
229
  <div class="preview-box">
230
- <div class="preview-header">
231
- <span>PNG Preview</span>
232
- <button class="btn-download" onclick="process('download', 'png')">Download .png</button>
233
- </div>
234
- <div id="png-prev" class="preview-content" style="background:#f0f0f0; text-align:center;"></div>
235
  </div>
236
  </div>
237
  </div>
238
 
239
  <script>
240
  async function analyze() {
241
- const btn = document.getElementById('load-btn');
242
- const text = document.getElementById('md-input').value;
243
- if(!text) return alert("Please enter some text first.");
244
-
245
- btn.innerText = "Analyzing...";
246
- const fd = new FormData();
247
- fd.append('markdown_text', text);
248
-
249
- try {
250
- const res = await fetch('/parse', {method:'POST', body:fd});
251
- const data = await res.json();
252
- if(data.error) throw new Error(data.error);
253
-
254
- document.getElementById('detected-format').innerText = "FORMAT DETECTED: " + data.format;
255
- const list = document.getElementById('comp-list');
256
- list.innerHTML = '';
257
-
258
- data.components.forEach((c, i) => {
259
- const safe = btoa(unescape(encodeURIComponent(c.content)));
260
- list.innerHTML += `
261
- <div class="comp-card">
262
- <label style="cursor:pointer; display:block;">
263
- <input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}">
264
- <strong>${c.filename}</strong>
265
- </label>
266
- <small>${c.type}</small>
267
- <textarea readonly>${c.content.substring(0,120)}...</textarea>
268
- </div>`;
269
- });
270
- document.getElementById('comp-section').style.display = 'block';
271
- } catch(e) { alert(e.message); }
272
- finally { btn.innerText = "Analyze & Extract Components"; }
273
  }
274
 
275
  async function process(action, type = null) {
276
  const btn = document.getElementById('gen-btn');
277
- let finalMarkdown = "";
278
- const checks = document.querySelectorAll('.c-check');
279
-
280
- if(checks.length > 0) {
281
- checks.forEach(c => {
282
- if(c.checked) {
283
- const content = decodeURIComponent(escape(atob(c.dataset.content)));
284
- if(!c.dataset.name.includes("Intro") && !c.dataset.name.includes("Structure")) {
285
- finalMarkdown += "### File: " + c.dataset.name + "\\n";
286
- }
287
- finalMarkdown += content + "\\n\\n";
288
- }
289
- });
290
- } else {
291
- finalMarkdown = document.getElementById('md-input').value;
292
- }
293
 
294
  const payload = {
295
- markdown_text: finalMarkdown,
296
  download: action === 'download',
297
  download_type: type,
298
  styles: {
@@ -303,37 +277,23 @@ def index():
303
  background_color: document.getElementById('b_color').value,
304
  highlight_theme: document.getElementById('h_theme').value,
305
  custom_css: document.getElementById('c_css').value,
306
- page_padding: 40,
307
- code_padding: 15
308
  }
309
  };
310
 
311
- if(action === 'preview') btn.innerText = "Processing...";
312
-
313
- try {
314
- const res = await fetch('/convert', {
315
- method: 'POST',
316
- headers: {'Content-Type':'application/json'},
317
- body: JSON.stringify(payload)
318
- });
319
-
320
- if(action === 'download') {
321
- const blob = await res.blob();
322
- const url = window.URL.createObjectURL(blob);
323
- const a = document.createElement('a');
324
- a.href = url;
325
- a.download = `output_${new Date().getTime()}.${type}`;
326
- a.click();
327
- } else {
328
- const data = await res.json();
329
- if(data.error) throw new Error(data.error);
330
- document.getElementById('preview-area').style.display = 'block';
331
- document.getElementById('html-prev').innerHTML = data.preview_html;
332
- document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" style="max-width:100%; box-shadow: 0 0 20px rgba(0,0,0,0.2);">`;
333
- window.scrollTo({ top: document.getElementById('preview-area').offsetTop, behavior: 'smooth' });
334
- }
335
- } catch(e) { alert("Error: " + e.message); }
336
- finally { if(action === 'preview') btn.innerText = "GENERATE PREVIEW"; }
337
  }
338
  </script>
339
  </body></html>
 
17
 
18
  def parse_repo2markdown(text):
19
  components = []
 
20
  struct_match = re.search(r'## File Structure\n([\s\S]*?)(?=\n### File:|\Z)', text)
21
  if struct_match:
22
  components.append({'type': 'structure', 'filename': 'File Structure', 'content': struct_match.group(1).strip()})
23
 
 
24
  pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
25
  for match in pattern.finditer(text):
26
  filename = match.group(1).strip()
 
43
  components = []
44
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
45
  if parts[0].strip():
46
+ components.append({'type': 'intro', 'filename': 'Header', 'content': parts[0].strip()})
47
  for i in range(1, len(parts), 2):
48
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
49
  return components
 
58
  components.append({'type': 'file', 'filename': match.group(1).strip(), 'content': match.group(2).strip()})
59
  return components
60
 
61
+ def build_full_html(markdown_text, styles, for_image=False):
62
  wrapper_id = "#output-wrapper"
63
+ font_family = styles.get('font_family', "sans-serif")
64
+
65
+ # If rendering for an image, we avoid external network calls to Google Fonts
66
+ # as wkhtmltoimage often fails on SSL handshakes in Docker.
67
+ google_font_link = ""
68
+ if not for_image:
69
+ google_font_name = font_family.split(',')[0].strip("'\"")
70
+ google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={google_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
71
 
72
  highlight_theme = styles.get('highlight_theme', 'monokai')
73
  pygments_css = ""
 
93
  {styles.get('custom_css', '')}
94
  """
95
  html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables', 'codehilite', 'nl2br'])
96
+ return f"<!DOCTYPE html><html><head><meta charset='UTF-8'>{google_font_link}<style>{scoped_css}</style></head><body><div id='output-wrapper'>{html_content}</div></body></html>"
 
97
 
98
  @app.route('/parse', methods=['POST'])
99
  def parse_endpoint():
 
117
  def convert_endpoint():
118
  data = request.json
119
  try:
120
+ # Generate two versions: one for UI (with external fonts) and one for Image (clean)
121
+ preview_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=False)
122
+ image_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=True)
123
+
124
+ options = {
125
+ "quiet": "",
126
+ "encoding": "UTF-8",
127
+ "width": 1024,
128
+ "disable-smart-width": "",
129
+ "load-error-handling": "ignore", # CRITICAL: Prevents crash on minor network errors
130
+ "load-media-error-handling": "ignore"
131
+ }
132
 
133
  if data.get('download', False):
134
  if data.get('download_type') == 'html':
135
+ return send_file(BytesIO(preview_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
136
+ png_bytes = imgkit.from_string(image_html, False, options=options)
137
  return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
138
 
139
+ png_bytes = imgkit.from_string(image_html, False, options=options)
140
  return jsonify({
141
+ 'preview_html': preview_html,
142
  'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')
143
  })
144
  except Exception as e:
 
156
  :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; --primary: #5a32a3; }
157
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
158
  body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: var(--bg); color: var(--text); line-height: 1.4; transition: background 0.3s; }
 
159
  fieldset { border: 1px solid var(--border); background: var(--card); padding: 20px; margin-bottom: 25px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
160
  legend { font-weight: bold; padding: 0 10px; color: var(--primary); }
161
  textarea { width: 100%; border-radius: 6px; padding: 12px; border: 1px solid var(--border); background: var(--card); color: var(--text); font-family: 'Fira Code', monospace; box-sizing: border-box; }
162
  .format-banner { background: var(--primary); color: white; padding: 6px 16px; border-radius: 20px; font-size: 13px; display: inline-block; margin-bottom: 15px; font-weight: bold; }
163
  .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
164
  .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
165
+ .comp-card { background: var(--card); border: 1px solid var(--border); padding: 12px; border-radius: 8px; }
166
  .comp-card textarea { height: 60px; font-size: 11px; margin-top: 8px; opacity: 0.8; pointer-events: none; resize: none; }
 
167
  .action-bar { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
168
  button { padding: 12px 24px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; transition: opacity 0.2s; }
169
  .btn-primary { background: var(--primary); color: #fff; flex: 1; min-width: 200px; }
170
  .btn-secondary { background: #333; color: #fff; border: 1px solid #555; }
171
+ .btn-download { background: #28a745; color: white; font-size: 12px; padding: 5px 10px; }
172
  .preview-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
173
+ .preview-box { background: #fff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; height: 600px; display: flex; flex-direction: column; }
174
+ .preview-header { background: #eee; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; color: #333; font-weight:bold; }
175
  .preview-content { flex: 1; overflow: auto; padding: 15px; }
 
176
  </style>
177
  </head>
178
  <body>
 
183
 
184
  <fieldset>
185
  <legend>1. Input Source</legend>
186
+ <textarea id="md-input" rows="10" placeholder="Paste Markdown text here..."></textarea>
187
  <div class="action-bar"><button onclick="analyze()" class="btn-primary" id="load-btn">Analyze & Extract Components</button></div>
188
  </fieldset>
189
 
 
198
  <fieldset>
199
  <legend>3. Visual Styling</legend>
200
  <div class="style-grid">
201
+ <div><label>Font</label>
202
  <select id="f_family">
203
+ <option value="Arial, sans-serif">Arial</option>
204
+ <option value="'Courier New', monospace">Courier</option>
205
+ <option value="'Times New Roman', serif">Times</option>
 
206
  </select>
207
  </div>
208
+ <div><label>Font Size</label><input type="number" id="f_size" value="16"></div>
209
+ <div><label>Line Height</label><input type="number" id="l_height" value="1.6" step="0.1"></div>
210
+ <div><label>Text Color</label><input type="color" id="t_color" value="#333333"></div>
211
+ <div><label>Background</label><input type="color" id="b_color" value="#ffffff"></div>
212
+ <div><label>Syntax</label>
213
  <select id="h_theme">
214
  {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
215
  </select>
216
  </div>
217
  </div>
218
+ <div style="margin-top:15px;">
219
  <label><b>Custom CSS Overrides</b></label>
220
+ <textarea id="c_css" rows="3" placeholder="#output-wrapper h1 { color: red; }"></textarea>
221
  </div>
222
  </fieldset>
223
 
224
+ <button onclick="process('preview')" class="btn-primary" id="gen-btn" style="width:100%; height:60px; font-size:20px;">GENERATE PREVIEW</button>
 
 
225
 
226
  <div id="preview-area" style="display:none;">
227
  <div class="preview-section">
228
  <div class="preview-box">
229
+ <div class="preview-header">HTML Preview <button class="btn-download" onclick="process('download', 'html')">Download</button></div>
 
 
 
230
  <div id="html-prev" class="preview-content"></div>
231
  </div>
232
  <div class="preview-box">
233
+ <div class="preview-header">PNG Preview <button class="btn-download" onclick="process('download', 'png')">Download</button></div>
234
+ <div id="png-prev" class="preview-content" style="background:#888; display:flex; align-items:center; justify-content:center;"></div>
 
 
 
235
  </div>
236
  </div>
237
  </div>
238
 
239
  <script>
240
  async function analyze() {
241
+ const fd = new FormData(); fd.append('markdown_text', document.getElementById('md-input').value);
242
+ const res = await fetch('/parse', {method:'POST', body:fd});
243
+ const data = await res.json();
244
+ document.getElementById('detected-format').innerText = "FORMAT: " + data.format;
245
+ const list = document.getElementById('comp-list'); list.innerHTML = '';
246
+ data.components.forEach(c => {
247
+ const safe = btoa(unescape(encodeURIComponent(c.content)));
248
+ list.innerHTML += `<div class="comp-card">
249
+ <label><input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}"> <b>${c.filename}</b></label>
250
+ <textarea readonly>${c.content.substring(0,100)}</textarea>
251
+ </div>`;
252
+ });
253
+ document.getElementById('comp-section').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
 
256
  async function process(action, type = null) {
257
  const btn = document.getElementById('gen-btn');
258
+ let md = "";
259
+ document.querySelectorAll('.c-check').forEach(c => {
260
+ if(c.checked) {
261
+ const content = decodeURIComponent(escape(atob(c.dataset.content)));
262
+ if(!c.dataset.name.includes("Intro") && !c.dataset.name.includes("Structure")) md += "### File: " + c.dataset.name + "\\n";
263
+ md += content + "\\n\\n";
264
+ }
265
+ });
266
+ if(!md) md = document.getElementById('md-input').value;
 
 
 
 
 
 
 
267
 
268
  const payload = {
269
+ markdown_text: md,
270
  download: action === 'download',
271
  download_type: type,
272
  styles: {
 
277
  background_color: document.getElementById('b_color').value,
278
  highlight_theme: document.getElementById('h_theme').value,
279
  custom_css: document.getElementById('c_css').value,
280
+ page_padding: 40, code_padding: 15
 
281
  }
282
  };
283
 
284
+ if(action === 'preview') btn.innerText = "Generating...";
285
+ const res = await fetch('/convert', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
286
+
287
+ if(action === 'download') {
288
+ const blob = await res.blob();
289
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `file.${type}`; a.click();
290
+ } else {
291
+ const data = await res.json();
292
+ document.getElementById('preview-area').style.display = 'block';
293
+ document.getElementById('html-prev').innerHTML = data.preview_html;
294
+ document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" style="max-width:100%;">`;
295
+ btn.innerText = "GENERATE PREVIEW";
296
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
  </script>
299
  </body></html>