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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +466 -90
app.py CHANGED
@@ -9,6 +9,150 @@ from dotenv import load_dotenv
9
  import gradio as gr
10
  import base64
11
  from io import BytesIO
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # ========== LOAD ENVIRONMENT VARIABLES ==========
14
  def load_environment_variables():
@@ -31,7 +175,10 @@ def load_environment_variables():
31
 
32
  # ========== FUNCTIONS ==========
33
  def parse_gemini_content(content, post_status='draft'):
34
- """Parse Gemini content for cdgarment.com with Rank Math"""
 
 
 
35
  data = {
36
  'seo_title': '',
37
  'primary_keyword': '',
@@ -44,35 +191,67 @@ def parse_gemini_content(content, post_status='draft'):
44
  }
45
 
46
  try:
47
- # Extract SEO Title
48
- seo_title_match = re.search(r'SEO Title:\s*(.+?)(?=\n)', content)
 
49
  if seo_title_match:
50
  data['seo_title'] = seo_title_match.group(1).strip()
51
 
52
- # Extract Primary Keyword
53
- keyword_match = re.search(r'Primary Keyword:\s*(.+?)(?=\n)', content)
54
  if keyword_match:
55
  data['primary_keyword'] = keyword_match.group(1).strip()
56
 
57
- # Extract Meta Description
58
- meta_match = re.search(r'Meta Description:\s*(.+?)(?=\n)', content)
 
59
  if meta_match:
60
  data['meta_description'] = meta_match.group(1).strip()
61
 
62
- # Extract Tags
63
- tags_match = re.search(r'Tags:\s*(.+?)(?=\n\|📝)', content)
64
- if tags_match:
65
- tags_str = tags_match.group(1).strip()
66
- data['tags'] = [tag.strip() for tag in re.split(r'[,;]', tags_str) if tag.strip()]
67
-
68
- # Extract Article Content
69
- article_match = re.search(r'(?:📝\|▶\|●\|◆).*?(?:Article\|Content)[:\-]?\s*(.+)', content, re.DOTALL)
70
- if article_match:
71
- full_content = article_match.group(1).strip()
72
- lines = full_content.split('\n')
73
- if lines:
74
- data['article_title'] = lines[0].strip()
75
- data['content'] = '\n'.join(lines[1:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  # Generate URL slug
78
  if data['seo_title']:
@@ -85,7 +264,7 @@ def parse_gemini_content(content, post_status='draft'):
85
  if not data['seo_title'] and data['article_title']:
86
  data['seo_title'] = data['article_title']
87
 
88
- # Prepare Rank Math meta fields (based on your screenshot)
89
  data['rank_math_meta'] = {
90
  'rank_math_title': data['seo_title'],
91
  'rank_math_description': data['meta_description'],
@@ -98,13 +277,14 @@ def parse_gemini_content(content, post_status='draft'):
98
  'rank_math_canonical_url': '',
99
  }
100
 
101
- # If we have primary keyword, add it (though not in your screenshot)
102
  if data['primary_keyword']:
103
  data['rank_math_meta']['rank_math_focus_keyword'] = data['primary_keyword']
104
 
105
  return data
106
 
107
  except Exception as e:
 
108
  return {'error': f"Error parsing: {str(e)}"}
109
 
110
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
@@ -229,37 +409,52 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='dr
229
  return None
230
 
231
  # ========== GRADIO UI FUNCTIONS ==========
232
- def parse_content(gemini_content, post_status):
233
- """Parse Gemini content and return preview"""
234
- if not gemini_content.strip():
235
- return "Please paste content first", "", "", "", "", "", {}
 
 
 
 
 
 
236
 
237
- parsed = parse_gemini_content(gemini_content, post_status)
238
 
239
  if 'error' in parsed:
240
- return parsed['error'], "", "", "", "", "", {}
241
 
242
- # Format tags for display - FIXED: Handle empty tags list
243
- if parsed.get('tags') and len(parsed['tags']) > 0:
244
- tags_display = ", ".join(parsed['tags'][:6]) # Show first 6 tags
 
 
 
 
 
 
 
 
245
  else:
246
- tags_display = "No tags found"
247
 
248
  # Format meta description preview
249
- meta_preview = parsed['meta_description'][:100] + "..." if len(parsed['meta_description']) > 100 else parsed['meta_description']
250
-
251
- # Create rank math preview text
252
- rank_math_preview = "\n".join([f"{key}: {value}" for key, value in parsed['rank_math_meta'].items()][:5])
253
 
254
- return (
255
- f"✅ Parsed: {parsed['seo_title']}",
256
- parsed['seo_title'],
257
- parsed['url_slug'],
258
  meta_preview,
259
- parsed['primary_keyword'],
260
  tags_display,
261
- f"Content Length: {len(parsed['content'])} chars"
262
- ), parsed
 
263
 
264
  def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_username, wp_password):
265
  """Publish post to WordPress"""
@@ -338,7 +533,7 @@ def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_use
338
  def clear_all():
339
  """Clear all inputs"""
340
  return (
341
- "", # gemini_content
342
  None, # image
343
  "https://cdgarment.com", # wp_url
344
  "", # wp_username
@@ -355,10 +550,40 @@ def clear_all():
355
  None, # download_file
356
  )
357
 
358
- # ========== GRADIO UI ==========
359
- with gr.Blocks(
360
- title="CdGarment WordPress Publisher"
361
- ) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
  # State variables
364
  parsed_data_state = gr.State(None)
@@ -375,33 +600,153 @@ with gr.Blocks(
375
  # Two main columns
376
  with gr.Row():
377
  with gr.Column(scale=2):
378
- with gr.Group():
379
- gr.Markdown("### 📋 Paste Gemini Content")
380
- gr.Markdown("Include SEO package and article")
381
-
382
- # Example content
383
- example_content = """SEO Toolkit: Article #20
384
- Element Suggestion
385
- Primary Keyword: Apparel ODM Design Services
386
- SEO Title: The Creative Engine: Inside CdGarment's Powerful R&D Team for ODM Success
387
- Meta Description: Elevate your brand with CdGarment's R&D team. We provide expert ODM services, from trend analysis to prototype development, ensuring your vision becomes reality.
388
- Tags: apparel ODM partner, fashion R&D team, original design manufacturer, garment prototype development, CdGarment R&D, fashion trend development
389
- 📝 Article #20: Full Content
390
- The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
391
-
392
- [Paste your complete Gemini output here]"""
393
-
394
- gemini_content = gr.Textbox(
395
- label="Gemini Content",
396
- value=example_content,
397
- lines=20,
398
- placeholder="Paste your complete Gemini output here with all formatting",
399
- elem_classes="paste-box"
400
- )
401
-
402
- with gr.Row():
403
- parse_btn = gr.Button("🔍 Parse Content", variant="primary", scale=2)
404
- clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  with gr.Column(scale=1):
407
  # Image upload section
@@ -513,8 +858,9 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
513
  return gr.update(visible=False), False
514
 
515
  # Parse button handler
516
- def on_parse_click(gemini_content_val, post_status_val):
517
- if not gemini_content_val.strip():
 
518
  return (
519
  gr.update(value="Please paste content first", visible=True),
520
  gr.update(visible=False),
@@ -523,10 +869,11 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
523
  *update_status_indicators(False, False, False, post_status_val)
524
  )
525
 
526
- result, parsed = parse_content(gemini_content_val, post_status_val)
527
 
528
  if isinstance(result, tuple):
529
- parse_status_val, seo_title_val, url_slug_val, meta_desc_val, keyword_val, tags_val, content_len_val = result
 
530
  return (
531
  gr.update(value=parse_status_val, visible=True),
532
  gr.update(visible=True),
@@ -615,8 +962,8 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
615
  )
616
 
617
  # Update status indicators when inputs change
618
- def update_indicators_on_change(gemini_content_val, image_file, wp_url_val, wp_username_val, wp_password_val, post_status_val, parsed_data):
619
- has_content = bool(parsed_data) or bool(gemini_content_val and gemini_content_val.strip())
620
  has_image = bool(image_file)
621
  has_wp = check_wp_config(wp_url_val, wp_username_val, wp_password_val)
622
  return update_status_indicators(has_content, has_image, has_wp, post_status_val)
@@ -633,7 +980,7 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
633
  # Parse button
634
  parse_btn.click(
635
  on_parse_click,
636
- inputs=[gemini_content, post_status],
637
  outputs=[
638
  parse_status,
639
  preview_row,
@@ -665,11 +1012,11 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
665
  outputs=[publish_status, download_file, content_status, image_status, wp_status, status_indicator]
666
  )
667
 
668
- # Clear button
669
- clear_btn.click(
670
  clear_all,
671
  outputs=[
672
- gemini_content,
673
  image_input,
674
  wp_url,
675
  wp_username,
@@ -688,19 +1035,48 @@ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
688
  )
689
 
690
  # Update status indicators on input changes
691
- for input_component in [gemini_content, image_input, wp_url, wp_username, wp_password, post_status]:
692
  input_component.change(
693
  update_indicators_on_change,
694
- inputs=[gemini_content, image_input, wp_url, wp_username, wp_password, post_status, parsed_data_state],
695
  outputs=[content_status, image_status, wp_status, status_indicator]
696
  )
697
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  # Launch the Gradio app
699
  if __name__ == "__main__":
 
 
 
 
 
 
 
700
  demo.launch(
701
  server_name="0.0.0.0",
702
- server_port=7860,
703
- share=False,
704
  debug=True,
705
  theme=gr.themes.Soft(
706
  primary_hue="purple",
 
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():
 
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
  }
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
242
+ r'##\s+(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)' # After heading
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
 
256
  # Generate URL slug
257
  if data['seo_title']:
 
264
  if not data['seo_title'] and data['article_title']:
265
  data['seo_title'] = data['article_title']
266
 
267
+ # Prepare Rank Math meta fields
268
  data['rank_math_meta'] = {
269
  'rank_math_title': data['seo_title'],
270
  'rank_math_description': data['meta_description'],
 
277
  'rank_math_canonical_url': '',
278
  }
279
 
280
+ # Add primary keyword if available
281
  if data['primary_keyword']:
282
  data['rank_math_meta']['rank_math_focus_keyword'] = data['primary_keyword']
283
 
284
  return data
285
 
286
  except Exception as e:
287
+ print(f"Error parsing: {str(e)}")
288
  return {'error': f"Error parsing: {str(e)}"}
289
 
290
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
 
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'], "", "", "", "", "", "", {}
427
 
428
+ # Ensure all fields exist
429
+ parsed.setdefault('seo_title', 'No title')
430
+ parsed.setdefault('meta_description', '')
431
+ parsed.setdefault('primary_keyword', '')
432
+ parsed.setdefault('tags', [])
433
+ parsed.setdefault('content', '')
434
+ parsed.setdefault('url_slug', '')
435
+
436
+ # Format tags for display
437
+ if parsed['tags'] and len(parsed['tags']) > 0:
438
+ tags_display = ", ".join(parsed['tags'][:8])
439
  else:
440
+ tags_display = "No tags found in content"
441
 
442
  # Format meta description preview
443
+ if parsed['meta_description']:
444
+ meta_preview = parsed['meta_description'][:120] + "..." if len(parsed['meta_description']) > 120 else parsed['meta_description']
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"""
 
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
 
550
  None, # download_file
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)
 
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
 
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
  *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),
 
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
  # 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
  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
  )
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",