AllanHill commited on
Commit
f853bf2
·
verified ·
1 Parent(s): 8b1b70c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +235 -497
app.py CHANGED
@@ -2,160 +2,29 @@ import requests
2
  import re
3
  import json
4
  from datetime import datetime
5
- from PIL import Image
6
- import io
7
  import os
8
- from dotenv import load_dotenv
9
  import gradio as gr
10
- import base64
11
- from io import BytesIO
12
- import html
13
 
14
- # Quill.js Editor Implementation
15
- class QuillEditor(gr.components.Component):
16
- """Custom Quill.js rich text editor component"""
17
-
18
- def __init__(self, value="", **kwargs):
19
- super().__init__(value=value, **kwargs)
20
-
21
- def get_config(self):
 
 
 
22
  return {
23
- "value": self.value,
24
- **super().get_config()
 
 
25
  }
26
 
27
- @staticmethod
28
- def update(value=None, **kwargs):
29
- return gr.update(value=value, **kwargs)
30
-
31
- # Quill.js HTML template
32
- quill_template = """
33
- <div id="quill-editor-{id}" style="height: 500px; border: 2px solid #3B82F6; border-radius: 10px;"></div>
34
- <input type="hidden" id="quill-input-{id}" name="{id}" value="{value}">
35
-
36
- <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
37
- <script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
38
-
39
- <script>
40
- // Initialize Quill editor
41
- var quill = new Quill('#quill-editor-{id}', {
42
- theme: 'snow',
43
- modules: {
44
- toolbar: [
45
- ['bold', 'italic', 'underline', 'strike'],
46
- ['blockquote', 'code-block'],
47
- [{ 'header': 1 }, { 'header': 2 }],
48
- [{ 'list': 'ordered'}, { 'list': 'bullet' }],
49
- [{ 'script': 'sub'}, { 'script': 'super' }],
50
- [{ 'indent': '-1'}, { 'indent': '+1' }],
51
- [{ 'direction': 'rtl' }],
52
- [{ 'size': ['small', false, 'large', 'huge'] }],
53
- [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
54
- [{ 'color': [] }, { 'background': [] }],
55
- [{ 'font': [] }],
56
- [{ 'align': [] }],
57
- ['clean'],
58
- ['link', 'image', 'video', 'formula']
59
- ]
60
- },
61
- placeholder: 'Paste your Gemini content here...'
62
- });
63
-
64
- // Set initial content
65
- quill.root.innerHTML = `{value}`;
66
-
67
- // Update hidden input on change
68
- quill.on('text-change', function() {
69
- document.getElementById('quill-input-{id}').value = quill.root.innerHTML;
70
- });
71
-
72
- // Handle paste events to preserve Word formatting
73
- quill.clipboard.addMatcher(Node.ELEMENT_NODE, function(node, delta) {
74
- // Preserve tables from Word
75
- if (node.tagName === 'TABLE') {
76
- let tableHTML = node.outerHTML;
77
- return new Quill.imports.delta().insert(tableHTML, { 'html': true });
78
- }
79
- // Preserve lists
80
- if (node.tagName === 'UL' || node.tagName === 'OL') {
81
- let listHTML = node.outerHTML;
82
- return new Quill.imports.delta().insert(listHTML, { 'html': true });
83
- }
84
- return delta;
85
- });
86
- </script>
87
- """
88
-
89
- # ========== HTML TO TEXT CONVERSION ==========
90
- def html_to_text(html_content):
91
- """Convert HTML from Quill editor to plain text for parsing"""
92
- if not html_content:
93
- return ""
94
-
95
- # Decode HTML entities
96
- text = html.unescape(html_content)
97
-
98
- # Remove HTML tags but preserve structure
99
- # Replace common HTML tags with appropriate line breaks
100
- text = re.sub(r'<br\s*/?>', '\n', text)
101
- text = re.sub(r'<p[^>]*>', '', text)
102
- text = re.sub(r'</p>', '\n', text)
103
- text = re.sub(r'<div[^>]*>', '', text)
104
- text = re.sub(r'</div>', '\n', text)
105
- text = re.sub(r'<h[1-6][^>]*>', '', text)
106
- text = re.sub(r'</h[1-6]>', '\n', text)
107
- text = re.sub(r'<strong[^>]*>', '', text)
108
- text = re.sub(r'</strong>', '', text)
109
- text = re.sub(r'<b[^>]*>', '', text)
110
- text = re.sub(r'</b>', '', text)
111
- text = re.sub(r'<em[^>]*>', '', text)
112
- text = re.sub(r'</em>', '', text)
113
- text = re.sub(r'<i[^>]*>', '', text)
114
- text = re.sub(r'</i>', '', text)
115
- text = re.sub(r'<u[^>]*>', '', text)
116
- text = re.sub(r'</u>', '', text)
117
-
118
- # Remove all remaining HTML tags
119
- text = re.sub(r'<[^>]+>', '', text)
120
-
121
- # Normalize whitespace
122
- text = re.sub(r'\n\s*\n', '\n\n', text)
123
- text = text.strip()
124
-
125
- return text
126
-
127
- # Alternative: Use BeautifulSoup for better HTML parsing
128
- def html_to_text_bs4(html_content):
129
- """Convert HTML to text using BeautifulSoup (more robust)"""
130
- try:
131
- from bs4 import BeautifulSoup
132
- soup = BeautifulSoup(html_content, 'html.parser')
133
-
134
- # Replace <br> with newlines
135
- for br in soup.find_all('br'):
136
- br.replace_with('\n')
137
-
138
- # Replace <p> with newlines
139
- for p in soup.find_all('p'):
140
- p.append('\n')
141
-
142
- # Replace <div> with newlines
143
- for div in soup.find_all('div'):
144
- if div.text.strip():
145
- div.append('\n')
146
-
147
- # Get text and clean up
148
- text = soup.get_text()
149
- text = re.sub(r'\n\s*\n', '\n\n', text)
150
- text = text.strip()
151
-
152
- return text
153
- except ImportError:
154
- # Fallback to regex method
155
- return html_to_text(html_content)
156
-
157
- # ========== LOAD ENVIRONMENT VARIABLES ==========
158
- def load_environment_variables():
159
  try:
160
  load_dotenv(dotenv_path='rank_cd.env')
161
  return {
@@ -175,10 +44,7 @@ def load_environment_variables():
175
 
176
  # ========== FUNCTIONS ==========
177
  def parse_gemini_content(content, post_status='draft'):
178
- """Parse Gemini content from Quill HTML editor"""
179
- # Convert HTML to plain text first
180
- plain_text = html_to_text_bs4(content) if '<' in content else content
181
-
182
  data = {
183
  'seo_title': '',
184
  'primary_keyword': '',
@@ -191,51 +57,48 @@ def parse_gemini_content(content, post_status='draft'):
191
  }
192
 
193
  try:
194
- # ===== SEO TITLE =====
195
- # Handle both "SEO Title:" and "SEO Title" (without colon) patterns
196
- seo_title_match = re.search(r'SEO\s*Title\s*:?\s*(.+?)(?=\n|$)', plain_text, re.IGNORECASE | re.DOTALL)
 
 
 
197
  if seo_title_match:
198
  data['seo_title'] = seo_title_match.group(1).strip()
199
 
200
- # ===== PRIMARY KEYWORD =====
201
- keyword_match = re.search(r'Primary\s*Keyword\s*:?\s*(.+?)(?=\n|$)', plain_text, re.IGNORECASE | re.DOTALL)
202
  if keyword_match:
203
  data['primary_keyword'] = keyword_match.group(1).strip()
204
 
205
- # ===== META DESCRIPTION =====
206
- # Look for Meta Description (can be multi-line)
207
- meta_match = re.search(r'Meta\s*Description\s*:?\s*(.+?)(?=\n\s*\n|\nCharacter|\nTags|\n---|$)', plain_text, re.IGNORECASE | re.DOTALL)
208
  if meta_match:
209
  data['meta_description'] = meta_match.group(1).strip()
210
 
211
- # ===== TAGS - Updated for Quill HTML =====
212
- # Handle various tag formats in HTML
213
  tags_patterns = [
214
  r'Tags\s*:?\s*(.+?)(?=\n\s*\n|\n---|\n##|\n📝|$)', # Tags: on same line
215
- r'<strong>Tags?</strong>\s*:?\s*(.+?)(?=\n|$)', # <strong>Tags</strong>: format
216
- r'Tags?\s*\n(.+?)(?=\n\s*\n|\n---|\n##|$)' # Tags on one line, tags on next
217
  ]
218
 
219
  tags_str = ""
220
  for pattern in tags_patterns:
221
- tags_match = re.search(pattern, plain_text, re.IGNORECASE | re.DOTALL)
222
  if tags_match:
223
  tags_str = tags_match.group(1).strip()
224
  break
225
 
226
  if tags_str:
227
- # Clean and split tags - handle commas, semicolons, or newlines
228
  tags_list = []
229
  for tag in re.split(r'[,;\n]', tags_str):
230
  cleaned_tag = tag.strip()
231
- # Remove any HTML tags that might remain
232
- cleaned_tag = re.sub(r'<[^>]+>', '', cleaned_tag)
233
  if cleaned_tag and cleaned_tag.lower() not in ['', 'tags', 'tags:']:
234
  tags_list.append(cleaned_tag)
235
  data['tags'] = tags_list
236
 
237
- # ===== ARTICLE CONTENT =====
238
- # Extract content after markers (handle both --- and emoji markers)
239
  content_patterns = [
240
  r'---\s*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After ---
241
  r'📝\s*Article[^\n]*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After emoji
@@ -243,13 +106,15 @@ def parse_gemini_content(content, post_status='draft'):
243
  ]
244
 
245
  for pattern in content_patterns:
246
- article_match = re.search(pattern, plain_text, re.DOTALL)
247
  if article_match:
248
  full_content = article_match.group(1).strip()
249
- # Get the first line as title, rest as content
250
  lines = full_content.split('\n')
251
  if lines:
252
- data['article_title'] = lines[0].strip()
 
 
 
253
  data['content'] = '\n'.join(lines[1:])
254
  break
255
 
@@ -290,16 +155,15 @@ def parse_gemini_content(content, post_status='draft'):
290
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
291
  """Upload image to WordPress with auto-naming"""
292
  try:
293
- # Handle Gradio image input (can be PIL Image or path)
294
- if hasattr(image_file, 'name'): # It's a file object
295
- file_extension = image_file.name.split('.')[-1].lower()
296
- image_data = image_file.read()
297
- elif isinstance(image_file, str): # It's a file path
298
- file_extension = image_file.split('.')[-1].lower()
299
  with open(image_file, 'rb') as f:
300
  image_data = f.read()
301
- else: # Assume it's already bytes
302
- image_data = image_file
 
 
 
303
 
304
  # Generate filename from slug
305
  filename = f"{filename_slug}.{file_extension}"
@@ -334,6 +198,7 @@ def upload_image_to_wordpress(image_file, wp_config, filename_slug):
334
 
335
  return media_data['id']
336
  else:
 
337
  return None
338
 
339
  except Exception as e:
@@ -402,6 +267,7 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='dr
402
 
403
  return post_result
404
  else:
 
405
  return None
406
 
407
  except Exception as e:
@@ -409,18 +275,12 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='dr
409
  return None
410
 
411
  # ========== GRADIO UI FUNCTIONS ==========
412
- def parse_content(html_content, post_status):
413
- """Parse content from Quill editor (HTML)"""
414
- if not html_content or not html_content.strip():
415
  return "Please paste content first", "", "", "", "", "", "", {}
416
 
417
- # If content is HTML (contains tags), convert to text first
418
- if '<' in html_content:
419
- plain_text = html_to_text_bs4(html_content)
420
- else:
421
- plain_text = html_content
422
-
423
- parsed = parse_gemini_content(plain_text, post_status)
424
 
425
  if 'error' in parsed:
426
  return parsed['error'], "", "", "", "", "", "", {}
@@ -445,21 +305,22 @@ def parse_content(html_content, post_status):
445
  else:
446
  meta_preview = "No meta description found"
447
 
448
- result_tuple = (
449
  f"✅ Parsed: {parsed['seo_title'][:50]}..." if len(parsed['seo_title']) > 50 else f"✅ Parsed: {parsed['seo_title']}",
450
  parsed['seo_title'] or "No title extracted",
451
  parsed['url_slug'] or "No slug generated",
452
  meta_preview,
453
  parsed['primary_keyword'] or "No primary keyword",
454
  tags_display,
455
- f"Content Length: {len(parsed['content'])} characters"
 
456
  )
457
- return result_tuple, parsed
458
 
459
  def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_username, wp_password):
460
  """Publish post to WordPress"""
461
  if not parsed_data:
462
- return "❌ No parsed content available. Please parse content first.", None
 
463
 
464
  # Use stored config or provided inputs
465
  if not wp_config:
@@ -472,7 +333,8 @@ def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_use
472
 
473
  # Validate config
474
  if not all([wp_config['url'], wp_config['username'], wp_config['password']]):
475
- return "❌ WordPress configuration incomplete. Please provide URL, username, and password.", None
 
476
 
477
  try:
478
  yield "🖼️ Uploading image to WordPress...", None
@@ -533,7 +395,7 @@ def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_use
533
  def clear_all():
534
  """Clear all inputs"""
535
  return (
536
- "", # quill_content
537
  None, # image
538
  "https://cdgarment.com", # wp_url
539
  "", # wp_username
@@ -551,39 +413,7 @@ def clear_all():
551
  )
552
 
553
  # ========== CREATE GRADIO INTERFACE ==========
554
- with gr.Blocks(title="CdGarment WordPress Publisher", css="""
555
- .gradio-container { max-width: 1400px !important; }
556
- .quill-container {
557
- border: 2px solid #3B82F6;
558
- border-radius: 10px;
559
- padding: 15px;
560
- background-color: #F8FAFC;
561
- min-height: 500px;
562
- }
563
- .header { text-align: center; margin: 20px 0; }
564
- .header h1 {
565
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
566
- -webkit-background-clip: text;
567
- -webkit-text-fill-color: transparent;
568
- font-size: 2.5rem;
569
- font-weight: 800;
570
- margin-bottom: 10px;
571
- }
572
- .badge {
573
- background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
574
- color: #991B1B;
575
- padding: 8px 16px;
576
- border-radius: 20px;
577
- font-weight: 600;
578
- display: inline-block;
579
- margin-bottom: 20px;
580
- }
581
- .status-indicator { padding: 6px 12px; border-radius: 12px; font-weight: 600; margin: 0 4px; }
582
- .status-indicator.success { background-color: #D1FAE5; color: #065F46; }
583
- .status-indicator.warning { background-color: #FEF3C7; color: #92400E; }
584
- .status-indicator.error { background-color: #FEE2E2; color: #991B1B; }
585
- .status-indicator.info { background-color: #DBEAFE; color: #1E40AF; }
586
- """) as demo:
587
 
588
  # State variables
589
  parsed_data_state = gr.State(None)
@@ -591,162 +421,151 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
591
 
592
  # Header
593
  gr.HTML("""
594
- <div class="header">
595
- <h1>🏭 CdGarment WordPress Publisher</h1>
596
- <div class="badge">✓ Optimized for Rank Math SEO</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  </div>
598
  """)
599
 
600
  # Two main columns
601
  with gr.Row():
602
  with gr.Column(scale=2):
603
- gr.Markdown("### 📋 Paste Gemini Content")
604
- gr.Markdown("Paste directly from Gemini - formatting preserved!")
605
-
606
- # ========== QUILL.JS RICH TEXT EDITOR ==========
607
- def create_quill_editor():
608
- """Create an editable Quill editor using a stable approach"""
609
- return gr.HTML("""
610
- <div style="width: 100%;">
611
- <div id="editor-container" style="height: 400px; border: 2px solid #3B82F6; border-radius: 10px; padding: 15px; background-color: #F8FAFC;">
612
- <div id="quill-editor"></div>
613
- </div>
614
-
615
- <textarea id="html-output" style="display: none;"></textarea>
616
-
617
- <script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
618
- <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
 
 
 
619
 
620
- <script>
621
- // Wait for the container to be ready
622
- function initEditor() {
623
- if (!document.getElementById('quill-editor')) {
624
- setTimeout(initEditor, 100);
625
- return;
626
- }
627
-
628
- // Initialize Quill as global variable
629
- window.quill = new Quill('#quill-editor', {
630
- theme: 'snow',
631
- modules: {
632
- toolbar: [
633
- ['bold', 'italic', 'underline'],
634
- [{ 'header': [1, 2, 3, false] }],
635
- [{ 'list': 'ordered'}, { 'list': 'bullet' }],
636
- ['link', 'clean']
637
- ]
638
- },
639
- placeholder: 'Paste your Gemini content here...'
640
- });
641
-
642
- // Make it focusable and editable
643
- const editorEl = document.getElementById('quill-editor');
644
- if (editorEl) {
645
- editorEl.style.minHeight = '400px';
646
- editorEl.setAttribute('contenteditable', 'true');
647
- }
648
-
649
- // Set initial content
650
- const initialContent = `<h2>SEO Toolkit: Article #21</h2>
651
- <p><strong>Element</strong><br>Suggestion</p>
652
- <p><strong>Primary Keyword</strong><br>Garment Supply Chain Security</p>
653
- <p><strong>SEO Title</strong><br>Guaranteed Delivery: How CdGarment Solves the Global Sourcing Lead-Time Crisis</p>
654
- <p><strong>Meta Description</strong><br>Eliminate delivery risks with CdGarment. Our advanced automated facilities and "In-Time" communication guarantee reliable lead times and supply chain security.</p>
655
- <p><strong>Character Count</strong><br>156 Characters</p>
656
- <p><strong>Tags</strong><br>reliable garment supplier, apparel lead time management, secure supply chain fashion, automated apparel production, CdGarment delivery guarantee</p>
657
- <hr>
658
- <h2>Guaranteed Delivery: Solving the Lead-Time Crisis Through Automation</h2>
659
- <p>In 2026, the greatest risk to a fashion brand isn't design—it's <strong>delivery</strong>.</p>`;
660
-
661
- quill.root.innerHTML = initialContent;
662
- document.getElementById('html-output').value = initialContent;
663
-
664
- // Update output on change
665
- quill.on('text-change', function() {
666
- const html = quill.root.innerHTML;
667
- document.getElementById('html-output').value = html;
668
-
669
- // Dispatch event for Gradio to detect changes
670
- const event = new Event('input', { bubbles: true });
671
- document.getElementById('html-output').dispatchEvent(event);
672
- });
673
-
674
- // Handle paste events
675
- quill.clipboard.addMatcher(Node.ELEMENT_NODE, function(node, delta) {
676
- if (node.tagName === 'TABLE') {
677
- // Convert tables to text
678
- let tableText = '';
679
- node.querySelectorAll('tr').forEach(row => {
680
- const cells = [];
681
- row.querySelectorAll('td, th').forEach(cell => {
682
- cells.push(cell.textContent);
683
- });
684
- tableText += cells.join(' | ') + '\n';
685
- });
686
- return new Quill.imports.delta().insert(tableText);
687
- }
688
- return delta;
689
- });
690
- }
691
 
692
- // Initialize when page loads
693
- if (document.readyState === 'loading') {
694
- document.addEventListener('DOMContentLoaded', initEditor);
 
695
  } else {
696
- initEditor();
 
697
  }
698
- </script>
699
- </div>
700
- """)
701
-
702
- # Create the editor
703
- editor_html = create_quill_editor()
704
-
705
- # Hidden textarea to capture content
706
- quill_content = gr.Textbox(
707
- elem_id="html-output",
708
- visible=False,
709
- label="Editor Content"
710
- )
711
-
712
- with gr.Row():
713
- parse_btn = gr.Button("🔍 Parse Content", variant="primary", size="lg")
714
- clear_editor_btn = gr.Button("🗑️ Clear Editor")
715
- clear_all_btn = gr.Button("🗑️ Clear All")
716
-
717
- # Clear editor button handler
718
- def clear_editor_content():
719
- return ""
720
-
721
- clear_editor_btn.click(
722
- clear_editor_content,
723
- outputs=[quill_content],
724
- )
725
-
726
- # Add JavaScript to clear the Quill editor visually
727
- gr.HTML("""
728
- <script>
729
- function clearQuillEditor() {
730
- if (typeof quill !== 'undefined') {
731
- quill.root.innerHTML = '';
732
- // Also update the hidden input
733
- document.getElementById('html-output').value = '';
734
  }
735
- }
736
-
737
- // Add click event to clear button
738
- document.addEventListener('DOMContentLoaded', function() {
739
- setTimeout(function() {
740
- const clearBtns = document.querySelectorAll('button');
741
- clearBtns.forEach(btn => {
742
- if (btn.textContent.includes('Clear Editor')) {
743
- btn.addEventListener('click', clearQuillEditor);
744
- }
745
  });
746
- }, 1000); // Wait for buttons to be rendered
747
- });
748
- </script>
749
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
  with gr.Column(scale=1):
752
  # Image upload section
@@ -821,10 +640,10 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
821
 
822
  # Status indicators
823
  with gr.Row():
824
- content_status = gr.HTML("<span class='status-indicator warning'>❌ Content</span>")
825
- image_status = gr.HTML("<span class='status-indicator warning'>⚠️ Image Optional</span>")
826
- wp_status = gr.HTML("<span class='status-indicator warning'>❌ WordPress</span>")
827
- status_indicator = gr.HTML("<span class='status-indicator'>📊 Status: DRAFT</span>")
828
 
829
  publish_btn = gr.Button("🚀 PUSH TO WORDPRESS", variant="primary", size="lg")
830
 
@@ -840,10 +659,19 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
840
 
841
  def update_status_indicators(has_content, has_image, has_wp_config, post_status_value):
842
  """Update status indicators"""
843
- content_html = f"<span class='status-indicator success'>✅ Content</span>" if has_content else f"<span class='status-indicator error'>❌ Content</span>"
844
- image_html = f"<span class='status-indicator success'>✅ Image</span>" if has_image else f"<span class='status-indicator warning'>⚠️ Image Optional</span>"
845
- wp_html = f"<span class='status-indicator success'>✅ WordPress</span>" if has_wp_config else f"<span class='status-indicator error'>❌ WordPress</span>"
846
- status_html = f"<span class='status-indicator'>📊 Status: {post_status_value.upper()}</span>"
 
 
 
 
 
 
 
 
 
847
 
848
  return content_html, image_html, wp_html, status_html
849
 
@@ -858,9 +686,8 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
858
  return gr.update(visible=False), False
859
 
860
  # Parse button handler
861
- def on_parse_click(html_content, post_status_val):
862
- """Handle parse button click with Quill HTML input"""
863
- if not html_content or not html_content.strip():
864
  return (
865
  gr.update(value="Please paste content first", visible=True),
866
  gr.update(visible=False),
@@ -869,11 +696,15 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
869
  *update_status_indicators(False, False, False, post_status_val)
870
  )
871
 
872
- result, parsed = parse_content(html_content, post_status_val)
873
 
874
  if isinstance(result, tuple):
875
  parse_status_val, seo_title_val, url_slug_val, meta_desc_val, keyword_val, tags_val, content_len_val = result[:7]
876
 
 
 
 
 
877
  return (
878
  gr.update(value=parse_status_val, visible=True),
879
  gr.update(visible=True),
@@ -881,7 +712,7 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
881
  url_slug_val,
882
  meta_desc_val,
883
  keyword_val,
884
- tags_val,
885
  content_len_val,
886
  parsed,
887
  *update_status_indicators(True, False, False, post_status_val)
@@ -939,15 +770,8 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
939
  *update_status_indicators(True, bool(image_file), True, post_status_val)
940
  )
941
 
942
- # Simulate progress (Gradio doesn't have built-in progress bars for streaming)
943
- import time
944
- time.sleep(1)
945
-
946
  # Call the publish function
947
- publish_result = list(publish_post(parsed_data, wp_config_to_use, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val))
948
-
949
- if len(publish_result) > 1:
950
- status_msg, download_file_val = publish_result[-1]
951
  if download_file_val:
952
  yield (
953
  gr.update(value=status_msg),
@@ -960,10 +784,11 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
960
  gr.update(visible=False),
961
  *update_status_indicators(True, bool(image_file), True, post_status_val)
962
  )
 
963
 
964
  # Update status indicators when inputs change
965
- def update_indicators_on_change(quill_content_val, image_file, wp_url_val, wp_username_val, wp_password_val, post_status_val, parsed_data):
966
- has_content = bool(parsed_data) or bool(quill_content_val and quill_content_val.strip())
967
  has_image = bool(image_file)
968
  has_wp = check_wp_config(wp_url_val, wp_username_val, wp_password_val)
969
  return update_status_indicators(has_content, has_image, has_wp, post_status_val)
@@ -980,7 +805,7 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
980
  # Parse button
981
  parse_btn.click(
982
  on_parse_click,
983
- inputs=[quill_content, post_status],
984
  outputs=[
985
  parse_status,
986
  preview_row,
@@ -1012,11 +837,11 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
1012
  outputs=[publish_status, download_file, content_status, image_status, wp_status, status_indicator]
1013
  )
1014
 
1015
- # Clear all button
1016
- clear_all_btn.click(
1017
  clear_all,
1018
  outputs=[
1019
- quill_content,
1020
  image_input,
1021
  wp_url,
1022
  wp_username,
@@ -1035,113 +860,26 @@ with gr.Blocks(title="CdGarment WordPress Publisher", css="""
1035
  )
1036
 
1037
  # Update status indicators on input changes
1038
- for input_component in [quill_content, image_input, wp_url, wp_username, wp_password, post_status]:
1039
  input_component.change(
1040
  update_indicators_on_change,
1041
- inputs=[quill_content, image_input, wp_url, wp_username, wp_password, post_status, parsed_data_state],
1042
  outputs=[content_status, image_status, wp_status, status_indicator]
1043
  )
1044
 
1045
- def test_quill_parsing():
1046
- """Test that Quill HTML content parses correctly"""
1047
- html_content = """
1048
- <h2>SEO Toolkit: Article #21</h2>
1049
- <p><strong>Element</strong><br>Suggestion</p>
1050
- <p><strong>Primary Keyword</strong><br>Garment Supply Chain Security</p>
1051
- <p><strong>SEO Title</strong><br>Guaranteed Delivery: How CdGarment Solves the Global Sourcing Lead-Time Crisis</p>
1052
- <p><strong>Meta Description</strong><br>Eliminate delivery risks with CdGarment. Our advanced automated facilities and "In-Time" communication guarantee reliable lead times and supply chain security.</p>
1053
- <p><strong>Character Count</strong><br>156 Characters</p>
1054
- <p><strong>Tags</strong><br>reliable garment supplier, apparel lead time management, secure supply chain fashion, automated apparel production, CdGarment delivery guarantee</p>
1055
- <hr>
1056
- <h2>Guaranteed Delivery: Solving the Lead-Time Crisis Through Automation</h2>
1057
- <p>In 2026, the greatest risk to a fashion brand isn't design—it's <strong>delivery</strong>.</p>
1058
- """
1059
-
1060
- parsed = parse_gemini_content(html_content, 'draft')
1061
- print(f"SEO Title: {parsed.get('seo_title')}")
1062
- print(f"Tags: {parsed.get('tags')}")
1063
- print(f"Tags count: {len(parsed.get('tags', []))}")
1064
-
1065
- return parsed
1066
-
1067
- # Launch the Gradio app
1068
  if __name__ == "__main__":
1069
- test_result = test_quill_parsing()
1070
- print("\nTest Result:")
1071
- for key, value in test_result.items():
1072
- if key != 'rank_math_meta':
1073
- print(f"{key}: {value}")
1074
-
1075
- print("\nLaunching Gradio app...")
1076
  demo.launch(
1077
  server_name="0.0.0.0",
1078
  server_port=7862,
1079
  share=True,
1080
  debug=True,
1081
- theme=gr.themes.Soft(
1082
- primary_hue="purple",
1083
- secondary_hue="green",
1084
- ),
1085
  css="""
1086
- .gradio-container {
1087
- max-width: 1400px !important;
1088
- }
1089
- .container {
1090
- padding: 20px;
1091
- }
1092
- .header {
1093
- text-align: center;
1094
- margin-bottom: 20px;
1095
- }
1096
- .header h1 {
1097
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1098
- -webkit-background-clip: text;
1099
- -webkit-text-fill-color: transparent;
1100
- font-size: 2.5rem;
1101
- font-weight: 800;
1102
- margin-bottom: 10px;
1103
- }
1104
- .badge {
1105
- background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
1106
- color: #991B1B;
1107
- padding: 8px 16px;
1108
- border-radius: 20px;
1109
- font-weight: 600;
1110
- display: inline-block;
1111
- margin-bottom: 20px;
1112
- }
1113
- .paste-box {
1114
- border: 2px solid #3B82F6;
1115
- border-radius: 10px;
1116
- padding: 20px;
1117
- background-color: #F8FAFC;
1118
- min-height: 500px;
1119
- }
1120
- .preview-box {
1121
- background-color: #f8f9fa;
1122
- border-radius: 10px;
1123
- padding: 15px;
1124
- margin: 10px 0;
1125
- }
1126
- .status-indicator {
1127
- display: inline-block;
1128
- padding: 5px 10px;
1129
- border-radius: 5px;
1130
- font-weight: bold;
1131
- margin: 2px;
1132
- }
1133
- .success {
1134
- background-color: #d1fae5;
1135
- color: #065f46;
1136
- }
1137
- .warning {
1138
- background-color: #fef3c7;
1139
- color: #92400e;
1140
- }
1141
- .error {
1142
- background-color: #fee2e2;
1143
- color: #991b1b;
1144
  }
1145
- """,
1146
- ssr_mode=False
1147
  )
 
2
  import re
3
  import json
4
  from datetime import datetime
 
 
5
  import os
 
6
  import gradio as gr
7
+ from dotenv import load_dotenv
 
 
8
 
9
+ # ========== LOAD ENVIRONMENT VARIABLES ==========
10
+ def load_environment_variables():
11
+ """Load environment variables for both local and Hugging Face"""
12
+ # For Hugging Face Spaces (use environment variables)
13
+ huggingface_url = os.environ.get('WORDPRESS_URL')
14
+ huggingface_username = os.environ.get('WORDPRESS_USERNAME')
15
+ huggingface_password = os.environ.get('WORDPRESS_APP_PASSWORD')
16
+ huggingface_status = os.environ.get('DEFAULT_STATUS')
17
+
18
+ # If Hugging Face env vars exist, use them
19
+ if huggingface_url and huggingface_username:
20
  return {
21
+ 'url': huggingface_url,
22
+ 'username': huggingface_username,
23
+ 'password': huggingface_password or '',
24
+ 'status': huggingface_status or 'draft'
25
  }
26
 
27
+ # Fallback to local .env file for development
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  try:
29
  load_dotenv(dotenv_path='rank_cd.env')
30
  return {
 
44
 
45
  # ========== FUNCTIONS ==========
46
  def parse_gemini_content(content, post_status='draft'):
47
+ """Parse Gemini content for cdgarment.com with Rank Math"""
 
 
 
48
  data = {
49
  'seo_title': '',
50
  'primary_keyword': '',
 
57
  }
58
 
59
  try:
60
+ # Clean content
61
+ content = re.sub(r'\r\n', '\n', content)
62
+ content = re.sub(r'\n\s*\n', '\n\n', content)
63
+
64
+ # Extract SEO Title
65
+ seo_title_match = re.search(r'SEO\s*Title\s*:?\s*(.+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
66
  if seo_title_match:
67
  data['seo_title'] = seo_title_match.group(1).strip()
68
 
69
+ # Extract Primary Keyword
70
+ keyword_match = re.search(r'Primary\s*Keyword\s*:?\s*(.+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
71
  if keyword_match:
72
  data['primary_keyword'] = keyword_match.group(1).strip()
73
 
74
+ # Extract Meta Description
75
+ meta_match = re.search(r'Meta\s*Description\s*:?\s*(.+?)(?=\n\s*\n|\nCharacter|\nTags|\n---|$)', content, re.IGNORECASE | re.DOTALL)
 
76
  if meta_match:
77
  data['meta_description'] = meta_match.group(1).strip()
78
 
79
+ # Extract Tags
 
80
  tags_patterns = [
81
  r'Tags\s*:?\s*(.+?)(?=\n\s*\n|\n---|\n##|\n📝|$)', # Tags: on same line
82
+ r'Tags\s*\n(.+?)(?=\n\s*\n|\n---|\n##|$)' # Tags on one line, actual tags on next
 
83
  ]
84
 
85
  tags_str = ""
86
  for pattern in tags_patterns:
87
+ tags_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
88
  if tags_match:
89
  tags_str = tags_match.group(1).strip()
90
  break
91
 
92
  if tags_str:
93
+ # Clean and split tags
94
  tags_list = []
95
  for tag in re.split(r'[,;\n]', tags_str):
96
  cleaned_tag = tag.strip()
 
 
97
  if cleaned_tag and cleaned_tag.lower() not in ['', 'tags', 'tags:']:
98
  tags_list.append(cleaned_tag)
99
  data['tags'] = tags_list
100
 
101
+ # Extract Article Content
 
102
  content_patterns = [
103
  r'---\s*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After ---
104
  r'📝\s*Article[^\n]*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After emoji
 
106
  ]
107
 
108
  for pattern in content_patterns:
109
+ article_match = re.search(pattern, content, re.DOTALL)
110
  if article_match:
111
  full_content = article_match.group(1).strip()
 
112
  lines = full_content.split('\n')
113
  if lines:
114
+ # Remove markdown heading markers if present
115
+ title = lines[0].strip()
116
+ title = re.sub(r'^#+\s*', '', title)
117
+ data['article_title'] = title
118
  data['content'] = '\n'.join(lines[1:])
119
  break
120
 
 
155
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
156
  """Upload image to WordPress with auto-naming"""
157
  try:
158
+ # Handle file path from Gradio
159
+ if isinstance(image_file, str):
 
 
 
 
160
  with open(image_file, 'rb') as f:
161
  image_data = f.read()
162
+ file_extension = image_file.split('.')[-1].lower()
163
+ else:
164
+ # Handle file object
165
+ image_data = image_file.read()
166
+ file_extension = image_file.name.split('.')[-1].lower()
167
 
168
  # Generate filename from slug
169
  filename = f"{filename_slug}.{file_extension}"
 
198
 
199
  return media_data['id']
200
  else:
201
+ print(f"Image upload failed: {response.text}")
202
  return None
203
 
204
  except Exception as e:
 
267
 
268
  return post_result
269
  else:
270
+ print(f"Post creation failed: {response.text}")
271
  return None
272
 
273
  except Exception as e:
 
275
  return None
276
 
277
  # ========== GRADIO UI FUNCTIONS ==========
278
+ def parse_content(gemini_content, post_status):
279
+ """Parse Gemini content and return preview"""
280
+ if not gemini_content or not gemini_content.strip():
281
  return "Please paste content first", "", "", "", "", "", "", {}
282
 
283
+ parsed = parse_gemini_content(gemini_content, post_status)
 
 
 
 
 
 
284
 
285
  if 'error' in parsed:
286
  return parsed['error'], "", "", "", "", "", "", {}
 
305
  else:
306
  meta_preview = "No meta description found"
307
 
308
+ return (
309
  f"✅ Parsed: {parsed['seo_title'][:50]}..." if len(parsed['seo_title']) > 50 else f"✅ Parsed: {parsed['seo_title']}",
310
  parsed['seo_title'] or "No title extracted",
311
  parsed['url_slug'] or "No slug generated",
312
  meta_preview,
313
  parsed['primary_keyword'] or "No primary keyword",
314
  tags_display,
315
+ f"Content Length: {len(parsed['content'])} characters",
316
+ parsed
317
  )
 
318
 
319
  def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_username, wp_password):
320
  """Publish post to WordPress"""
321
  if not parsed_data:
322
+ yield "❌ No parsed content available. Please parse content first.", None
323
+ return
324
 
325
  # Use stored config or provided inputs
326
  if not wp_config:
 
333
 
334
  # Validate config
335
  if not all([wp_config['url'], wp_config['username'], wp_config['password']]):
336
+ yield "❌ WordPress configuration incomplete. Please provide URL, username, and password.", None
337
+ return
338
 
339
  try:
340
  yield "🖼️ Uploading image to WordPress...", None
 
395
  def clear_all():
396
  """Clear all inputs"""
397
  return (
398
+ "", # gemini_content
399
  None, # image
400
  "https://cdgarment.com", # wp_url
401
  "", # wp_username
 
413
  )
414
 
415
  # ========== CREATE GRADIO INTERFACE ==========
416
+ with gr.Blocks(title="CdGarment WordPress Publisher") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
  # State variables
419
  parsed_data_state = gr.State(None)
 
421
 
422
  # Header
423
  gr.HTML("""
424
+ <div style="text-align: center; margin: 20px 0;">
425
+ <h1 style="
426
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
427
+ -webkit-background-clip: text;
428
+ -webkit-text-fill-color: transparent;
429
+ font-size: 2.5rem;
430
+ font-weight: 800;
431
+ margin-bottom: 10px;
432
+ ">🏭 CdGarment WordPress Publisher</h1>
433
+ <div style="
434
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
435
+ color: #991B1B;
436
+ padding: 8px 16px;
437
+ border-radius: 20px;
438
+ font-weight: 600;
439
+ display: inline-block;
440
+ margin-bottom: 20px;
441
+ ">✓ Optimized for Rank Math SEO</div>
442
  </div>
443
  """)
444
 
445
  # Two main columns
446
  with gr.Row():
447
  with gr.Column(scale=2):
448
+ with gr.Group():
449
+ gr.Markdown("### 📋 Paste Gemini Content")
450
+ gr.Markdown("Include SEO package and article. Formatting preserved!")
451
+
452
+ # Formatting toolbar
453
+ gr.HTML("""
454
+ <div style="background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; border: 1px solid #e9ecef;">
455
+ <strong>Formatting:</strong>
456
+ <button class="format-btn" onclick="formatText('# ', 'H1')" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">H1</button>
457
+ <button class="format-btn" onclick="formatText('## ', 'H2')" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">H2</button>
458
+ <button class="format-btn" onclick="formatText('**', 'Bold')" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer; font-weight: bold;">B</button>
459
+ <button class="format-btn" onclick="formatText('*', 'Italic')" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer; font-style: italic;">I</button>
460
+ <button class="format-btn" onclick="formatText('- ', 'List')" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">• List</button>
461
+ </div>
462
+
463
+ <script>
464
+ function formatText(wrapText, type) {
465
+ const textarea = document.querySelector('textarea[aria-label="Gemini Content"]');
466
+ if (!textarea) return;
467
 
468
+ const start = textarea.selectionStart;
469
+ const end = textarea.selectionEnd;
470
+ const selected = textarea.value.substring(start, end);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
+ if (selected) {
473
+ textarea.value = textarea.value.substring(0, start) + wrapText + selected + wrapText + textarea.value.substring(end);
474
+ textarea.selectionStart = start + wrapText.length;
475
+ textarea.selectionEnd = end + wrapText.length;
476
  } else {
477
+ textarea.value = textarea.value.substring(0, start) + wrapText + type + wrapText + textarea.value.substring(end);
478
+ textarea.selectionStart = textarea.selectionEnd = start + wrapText.length + type.length;
479
  }
480
+ textarea.focus();
481
+
482
+ const event = new Event('input', { bubbles: true });
483
+ textarea.dispatchEvent(event);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  }
485
+
486
+ document.addEventListener('DOMContentLoaded', function() {
487
+ const buttons = document.querySelectorAll('.format-btn');
488
+ buttons.forEach(btn => {
489
+ btn.addEventListener('click', function(e) {
490
+ e.preventDefault();
491
+ const wrap = this.getAttribute('onclick').match(/formatText\('([^']*)'/)[1];
492
+ const type = this.getAttribute('onclick').match(/formatText\('[^']*', '([^']*)'/)[1];
493
+ formatText(wrap, type);
494
+ });
495
  });
496
+ });
497
+ </script>
498
+ """)
499
+
500
+ # Markdown editor
501
+ example_content = """SEO Toolkit: Article #21
502
+
503
+ Element
504
+ Suggestion
505
+
506
+ Primary Keyword
507
+ Garment Supply Chain Security
508
+
509
+ SEO Title
510
+ Guaranteed Delivery: How CdGarment Solves the Global Sourcing Lead-Time Crisis
511
+
512
+ Meta Description
513
+ Eliminate delivery risks with CdGarment. Our advanced automated facilities and "In-Time" communication guarantee reliable lead times and supply chain security.
514
+
515
+ Character Count
516
+ 156 Characters
517
+
518
+ Tags
519
+ reliable garment supplier, apparel lead time management, secure supply chain fashion, automated apparel production, CdGarment delivery guarantee
520
+
521
+ ---
522
+
523
+ ## Guaranteed Delivery: Solving the Lead-Time Crisis Through Automation
524
+
525
+ In 2026, the greatest risk to a fashion brand isn't design—it's **delivery**. Port delays, labor shortages, and manual production bottlenecks have made traditional "medium" manufacturers unreliable.
526
+
527
+ CdGarment has solved this by rebuilding the manufacturing process around **Security and Stability**. When we commit to a date, our automated ecosystem ensures we meet it.
528
+
529
+ ### 1. Automation = Predictability
530
+
531
+ Manual factories suffer from "human variance." If a worker is sick or a bundle is lost, the whole line stops.
532
+
533
+ - **The Hanging System Advantage**: Our automated system ensures a continuous flow.
534
+ - **Buffer Management**: Our integrated bases allow us to shift production loads.
535
+
536
+ ### 2. Real-Time Supply Chain Visibility
537
+
538
+ Supply chain security comes from knowing, not guessing.
539
+
540
+ - **Digital Tracking**: Every order is logged in our system.
541
+ - **Proactive Logistics**: Our logistics team monitors global shipping lanes.
542
+
543
+ ### 3. Financial and Compliance Security
544
+
545
+ A secure supplier is a compliant supplier. CdGarment maintains all global certifications."""
546
+
547
+ gemini_content = gr.Textbox(
548
+ label="Gemini Content",
549
+ value=example_content,
550
+ lines=25,
551
+ placeholder="""Paste your Gemini output here...
552
+
553
+ Example format:
554
+ SEO Title: Your Title Here
555
+ Primary Keyword: Your Keyword
556
+ Meta Description: Your description
557
+ Tags: tag1, tag2, tag3
558
+
559
+ --- (or 📝 Article)
560
+
561
+ ## Article Title
562
+ Your article content here...""",
563
+ elem_classes="paste-box"
564
+ )
565
+
566
+ with gr.Row():
567
+ parse_btn = gr.Button("🔍 Parse Content", variant="primary", scale=2)
568
+ clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1)
569
 
570
  with gr.Column(scale=1):
571
  # Image upload section
 
640
 
641
  # Status indicators
642
  with gr.Row():
643
+ content_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fee2e2; color: #991b1b;'>❌ Content</span>")
644
+ image_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fef3c7; color: #92400e;'>⚠️ Image Optional</span>")
645
+ wp_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fee2e2; color: #991b1b;'>❌ WordPress</span>")
646
+ status_indicator = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #e5e7eb;'>📊 Status: DRAFT</span>")
647
 
648
  publish_btn = gr.Button("🚀 PUSH TO WORDPRESS", variant="primary", size="lg")
649
 
 
659
 
660
  def update_status_indicators(has_content, has_image, has_wp_config, post_status_value):
661
  """Update status indicators"""
662
+ content_color = "#d1fae5" if has_content else "#fee2e2"
663
+ content_text = " Content" if has_content else " Content"
664
+ content_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {content_color}; color: #065f46;'>{content_text}</span>"
665
+
666
+ image_color = "#d1fae5" if has_image else "#fef3c7"
667
+ image_text = "✅ Image" if has_image else "⚠️ Image Optional"
668
+ image_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {image_color}; color: #92400e;'>{image_text}</span>"
669
+
670
+ wp_color = "#d1fae5" if has_wp_config else "#fee2e2"
671
+ wp_text = "✅ WordPress" if has_wp_config else "❌ WordPress"
672
+ wp_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {wp_color}; color: #065f46;'>{wp_text}</span>"
673
+
674
+ status_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #e5e7eb;'>📊 Status: {post_status_value.upper()}</span>"
675
 
676
  return content_html, image_html, wp_html, status_html
677
 
 
686
  return gr.update(visible=False), False
687
 
688
  # Parse button handler
689
+ def on_parse_click(gemini_content_val, post_status_val):
690
+ if not gemini_content_val or not gemini_content_val.strip():
 
691
  return (
692
  gr.update(value="Please paste content first", visible=True),
693
  gr.update(visible=False),
 
696
  *update_status_indicators(False, False, False, post_status_val)
697
  )
698
 
699
+ result, parsed = parse_content(gemini_content_val, post_status_val)
700
 
701
  if isinstance(result, tuple):
702
  parse_status_val, seo_title_val, url_slug_val, meta_desc_val, keyword_val, tags_val, content_len_val = result[:7]
703
 
704
+ # Debug output
705
+ print(f"DEBUG - Parsed tags: {parsed.get('tags', [])}")
706
+ print(f"DEBUG - Tags display: {tags_val}")
707
+
708
  return (
709
  gr.update(value=parse_status_val, visible=True),
710
  gr.update(visible=True),
 
712
  url_slug_val,
713
  meta_desc_val,
714
  keyword_val,
715
+ tags_val if tags_val else "No tags extracted",
716
  content_len_val,
717
  parsed,
718
  *update_status_indicators(True, False, False, post_status_val)
 
770
  *update_status_indicators(True, bool(image_file), True, post_status_val)
771
  )
772
 
 
 
 
 
773
  # Call the publish function
774
+ for status_msg, download_file_val in publish_post(parsed_data, wp_config_to_use, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val):
 
 
 
775
  if download_file_val:
776
  yield (
777
  gr.update(value=status_msg),
 
784
  gr.update(visible=False),
785
  *update_status_indicators(True, bool(image_file), True, post_status_val)
786
  )
787
+ break
788
 
789
  # Update status indicators when inputs change
790
+ def update_indicators_on_change(gemini_content_val, image_file, wp_url_val, wp_username_val, wp_password_val, post_status_val, parsed_data):
791
+ has_content = bool(parsed_data) or bool(gemini_content_val and gemini_content_val.strip())
792
  has_image = bool(image_file)
793
  has_wp = check_wp_config(wp_url_val, wp_username_val, wp_password_val)
794
  return update_status_indicators(has_content, has_image, has_wp, post_status_val)
 
805
  # Parse button
806
  parse_btn.click(
807
  on_parse_click,
808
+ inputs=[gemini_content, post_status],
809
  outputs=[
810
  parse_status,
811
  preview_row,
 
837
  outputs=[publish_status, download_file, content_status, image_status, wp_status, status_indicator]
838
  )
839
 
840
+ # Clear button
841
+ clear_btn.click(
842
  clear_all,
843
  outputs=[
844
+ gemini_content,
845
  image_input,
846
  wp_url,
847
  wp_username,
 
860
  )
861
 
862
  # Update status indicators on input changes
863
+ for input_component in [gemini_content, image_input, wp_url, wp_username, wp_password, post_status]:
864
  input_component.change(
865
  update_indicators_on_change,
866
+ inputs=[gemini_content, image_input, wp_url, wp_username, wp_password, post_status, parsed_data_state],
867
  outputs=[content_status, image_status, wp_status, status_indicator]
868
  )
869
 
870
+ # ========== LAUNCH THE APP ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
  if __name__ == "__main__":
 
 
 
 
 
 
 
872
  demo.launch(
873
  server_name="0.0.0.0",
874
  server_port=7862,
875
  share=True,
876
  debug=True,
 
 
 
 
877
  css="""
878
+ .gradio-container { max-width: 1400px !important; }
879
+ .paste-box textarea {
880
+ font-family: 'Courier New', monospace !important;
881
+ font-size: 14px !important;
882
+ line-height: 1.5 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  }
884
+ """
 
885
  )