AllanHill commited on
Commit
b688b18
·
verified ·
1 Parent(s): 7ab4e9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +783 -429
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import streamlit as st
2
  import requests
3
  import re
4
  import json
@@ -7,115 +6,179 @@ from PIL import Image
7
  import io
8
  import os
9
  from dotenv import load_dotenv
 
 
 
 
10
 
11
- # ========== PAGE CONFIGURATION ==========
12
- st.set_page_config(
13
- page_title="CdGarment WordPress Publisher",
14
- page_icon="🏭",
15
- layout="wide",
16
- initial_sidebar_state="collapsed"
17
- )
 
 
 
 
 
 
 
 
 
18
 
19
- # ========== CUSTOM CSS ==========
20
- st.markdown("""
21
- <style>
22
- .main {
23
- padding: 2rem;
24
- }
25
- .main-header {
26
- font-size: 2.8rem;
27
- font-weight: 800;
28
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
29
- -webkit-background-clip: text;
30
- -webkit-text-fill-color: transparent;
31
- text-align: center;
32
- margin-bottom: 0.5rem;
33
- }
34
- .paste-box {
35
- border: 2px solid #3B82F6;
36
- border-radius: 10px;
37
- padding: 1.5rem;
38
- background-color: #F8FAFC;
39
- height: 500px;
40
- }
41
- .image-preview {
42
- border-radius: 10px;
43
- overflow: hidden;
44
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
45
- border: 2px solid #E5E7EB;
46
- max-height: 200px;
47
- width: 100%;
48
- object-fit: cover;
49
- }
50
- .publish-btn {
51
- background: linear-gradient(135deg, #059669 0%, #10B981 100%);
52
- color: white;
53
- font-size: 1.5rem;
54
- font-weight: 700;
55
- padding: 1.2rem;
56
- border: none;
57
- border-radius: 12px;
58
- width: 100%;
59
- cursor: pointer;
60
- transition: all 0.3s ease;
61
- box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3);
62
- margin-top: 1.5rem;
63
- }
64
- .publish-btn:hover {
65
- transform: translateY(-3px);
66
- box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
67
- }
68
- .publish-btn:disabled {
69
- background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%);
70
- cursor: not-allowed;
71
- transform: none;
72
- box-shadow: none;
73
  }
74
- .rank-math-badge {
75
- background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
76
- color: #991B1B;
77
- padding: 0.5rem 1rem;
78
- border-radius: 20px;
79
- font-weight: 600;
80
- display: inline-block;
81
- margin-bottom: 1rem;
82
  }
83
- </style>
84
- """, unsafe_allow_html=True)
 
 
85
 
86
- # ========== INITIALIZE SESSION STATE ==========
87
- for key in ['parsed_data', 'uploaded_image', 'wp_config', 'post_status', 'wp_config_locked', 'env_loaded']:
88
- if key not in st.session_state:
89
- if key == 'wp_config_locked':
90
- st.session_state[key] = False
91
- elif key == 'env_loaded':
92
- st.session_state[key] = False
93
- elif key == 'wp_config':
94
- st.session_state[key] = {}
95
- elif key == 'post_status':
96
- st.session_state[key] = 'draft'
97
- else:
98
- st.session_state[key] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  # ========== LOAD ENVIRONMENT VARIABLES ==========
101
- if not st.session_state.env_loaded:
102
  try:
103
  load_dotenv(dotenv_path='rank_cd.env')
104
- # Initialize wp_config from env if not already set
105
- if not st.session_state.wp_config:
106
- st.session_state.wp_config = {
107
- 'url': os.getenv('WORDPRESS_URL', 'https://cdgarment.com'),
108
- 'username': os.getenv('WORDPRESS_USERNAME', ''),
109
- 'password': os.getenv('WORDPRESS_APP_PASSWORD', ''),
110
- 'status': os.getenv('DEFAULT_STATUS', 'draft')
111
- }
112
- st.session_state.env_loaded = True
113
  except Exception as e:
114
- st.warning(f"Could not load rank_cd.env: {str(e)}")
 
 
 
 
 
 
115
 
116
  # ========== FUNCTIONS ==========
117
- def parse_gemini_content(content):
118
- """Parse Gemini content for cdgarment.com with Rank Math"""
 
 
 
119
  data = {
120
  'seo_title': '',
121
  'primary_keyword': '',
@@ -128,35 +191,67 @@ def parse_gemini_content(content):
128
  }
129
 
130
  try:
131
- # Extract SEO Title
132
- seo_title_match = re.search(r'SEO Title:\s*(.+?)(?=\n)', content)
 
133
  if seo_title_match:
134
  data['seo_title'] = seo_title_match.group(1).strip()
135
 
136
- # Extract Primary Keyword
137
- keyword_match = re.search(r'Primary Keyword:\s*(.+?)(?=\n)', content)
138
  if keyword_match:
139
  data['primary_keyword'] = keyword_match.group(1).strip()
140
 
141
- # Extract Meta Description
142
- meta_match = re.search(r'Meta Description:\s*(.+?)(?=\n)', content)
 
143
  if meta_match:
144
  data['meta_description'] = meta_match.group(1).strip()
145
 
146
- # Extract Tags
147
- tags_match = re.search(r'Tags:\s*(.+?)(?=\n|📝)', content)
148
- if tags_match:
149
- tags_str = tags_match.group(1).strip()
150
- data['tags'] = [tag.strip() for tag in re.split(r'[,;]', tags_str) if tag.strip()]
151
-
152
- # Extract Article Content
153
- article_match = re.search(r'(?:📝|▶|●|◆).*?(?:Article|Content)[:\-]?\s*(.+)', content, re.DOTALL)
154
- if article_match:
155
- full_content = article_match.group(1).strip()
156
- lines = full_content.split('\n')
157
- if lines:
158
- data['article_title'] = lines[0].strip()
159
- data['content'] = '\n'.join(lines[1:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  # Generate URL slug
162
  if data['seo_title']:
@@ -169,11 +264,11 @@ def parse_gemini_content(content):
169
  if not data['seo_title'] and data['article_title']:
170
  data['seo_title'] = data['article_title']
171
 
172
- # Prepare Rank Math meta fields (based on your screenshot)
173
  data['rank_math_meta'] = {
174
  'rank_math_title': data['seo_title'],
175
  'rank_math_description': data['meta_description'],
176
- 'rank_math_robots': ['index'] if st.session_state.post_status == 'publish' else ['noindex'],
177
  'rank_math_news_sitemap_robots': 'index',
178
  'rank_math_facebook_title': data['seo_title'],
179
  'rank_math_facebook_description': data['meta_description'],
@@ -182,26 +277,36 @@ def parse_gemini_content(content):
182
  'rank_math_canonical_url': '',
183
  }
184
 
185
- # If we have primary keyword, add it (though not in your screenshot)
186
  if data['primary_keyword']:
187
  data['rank_math_meta']['rank_math_focus_keyword'] = data['primary_keyword']
188
 
189
  return data
190
 
191
  except Exception as e:
192
- st.error(f"Error parsing: {str(e)}")
193
- return None
194
 
195
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
196
  """Upload image to WordPress with auto-naming"""
197
  try:
 
 
 
 
 
 
 
 
 
 
 
198
  # Generate filename from slug
199
- file_extension = image_file.name.split('.')[-1].lower()
200
  filename = f"{filename_slug}.{file_extension}"
201
 
202
  # Prepare image data
203
  files = {
204
- 'file': (filename, image_file.getvalue(), f'image/{file_extension}')
205
  }
206
 
207
  auth = (wp_config['username'], wp_config['password'])
@@ -229,14 +334,13 @@ def upload_image_to_wordpress(image_file, wp_config, filename_slug):
229
 
230
  return media_data['id']
231
  else:
232
- st.error(f"Image upload failed: {response.text}")
233
  return None
234
 
235
  except Exception as e:
236
- st.error(f"Error uploading image: {str(e)}")
237
  return None
238
 
239
- def create_wordpress_post(parsed_data, wp_config, media_id=None):
240
  """Create WordPress post with Rank Math meta"""
241
  try:
242
  # Base post data
@@ -244,7 +348,7 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None):
244
  'title': parsed_data['seo_title'],
245
  'content': parsed_data['content'],
246
  'slug': parsed_data['url_slug'],
247
- 'status': st.session_state.post_status,
248
  'meta': parsed_data['rank_math_meta']
249
  }
250
 
@@ -301,315 +405,99 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None):
301
  return None
302
 
303
  except Exception as e:
304
- st.error(f"Error creating post: {str(e)}")
305
  return None
306
 
307
- # ========== STREAMLIT UI ==========
308
- # Header
309
- st.markdown('<h1 class="main-header">🏭 CdGarment WordPress Publisher</h1>', unsafe_allow_html=True)
310
- st.markdown('<div class="rank-math-badge">✓ Optimized for Rank Math SEO</div>', unsafe_allow_html=True)
311
-
312
- # Two main columns
313
- col1, col2 = st.columns([2, 1], gap="large")
314
-
315
- # ========== LEFT COLUMN: CONTENT ==========
316
- with col1:
317
- st.markdown('<div class="paste-box">', unsafe_allow_html=True)
318
-
319
- st.markdown("### 📋 Paste Gemini Content")
320
- st.caption("Include SEO package and article")
321
-
322
- # Example content
323
- example_content = """SEO Toolkit: Article #20
324
- Element Suggestion
325
- Primary Keyword: Apparel ODM Design Services
326
- SEO Title: The Creative Engine: Inside CdGarment's Powerful R&D Team for ODM Success
327
- 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.
328
- Tags: apparel ODM partner, fashion R&D team, original design manufacturer, garment prototype development, CdGarment R&D, fashion trend development
329
- 📝 Article #20: Full Content
330
- The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
331
-
332
- [Paste your complete Gemini output here]"""
333
 
334
- gemini_content = st.text_area(
335
- "Paste your Gemini output below:",
336
- height=350,
337
- value=example_content,
338
- label_visibility="collapsed",
339
- placeholder="Paste your complete Gemini output here with all formatting",
340
- help="Formatting will be preserved exactly as pasted"
341
- )
342
-
343
- # Parse button
344
- if st.button("🔍 Parse Content", type="primary", use_container_width=True):
345
- if gemini_content.strip():
346
- with st.spinner("Parsing content and preparing Rank Math meta..."):
347
- parsed = parse_gemini_content(gemini_content)
348
- if parsed:
349
- st.session_state.parsed_data = parsed
350
- st.success(f"✅ Parsed: {parsed['seo_title']}")
351
- else:
352
- st.error("❌ Could not parse content")
353
- else:
354
- st.warning("Please paste content first")
355
-
356
- st.markdown('</div>', unsafe_allow_html=True)
357
-
358
- # ========== RIGHT COLUMN: IMAGE & SETTINGS ==========
359
- with col2:
360
- st.markdown("### 🖼️ Image & Upload")
361
-
362
- # Image preview box (always visible, empty when no image)
363
- st.markdown("**📷 Image Preview**")
364
-
365
- # Empty preview area with border
366
- if 'uploaded_image' not in st.session_state or not st.session_state.uploaded_image:
367
- # Show empty preview box
368
- st.markdown(
369
- '<div style="border: 2px dashed #8B5CF6; border-radius: 10px; padding: 2rem; text-align: center; background-color: #FAF5FF;">'
370
- '<div style="font-size: 3rem; margin-bottom: 1rem;">📁</div>'
371
- '<div style="color: #6B7280;">No image uploaded yet</div>'
372
- '</div>',
373
- unsafe_allow_html=True
374
- )
375
  else:
376
- # Show image preview
377
- st.image(
378
- st.session_state.uploaded_image,
379
- width=200,
380
- use_column_width=False
381
- )
382
-
383
- # Image upload button
384
- uploaded_image = st.file_uploader(
385
- "Upload image",
386
- type=['jpg', 'jpeg', 'png', 'gif', 'webp'],
387
- help="Image will be automatically named and added to post",
388
- label_visibility="collapsed"
389
- )
390
 
391
- # Handle image upload
392
- if uploaded_image:
393
- st.session_state.uploaded_image = uploaded_image
394
- st.rerun() # Refresh to show the preview
395
-
396
- # Image description field
397
- st.markdown("**🖋️ Image Description**")
398
- image_description = st.text_area(
399
- "Image description",
400
- height=100,
401
- placeholder="Auto-generated description will appear here",
402
- label_visibility="collapsed"
403
- )
404
 
405
- st.markdown("---")
406
- st.markdown("### ⚙️ WordPress Settings")
407
 
408
- # Get current values from session state
409
- current_config = st.session_state.wp_config or {
410
- 'url': 'https://cdgarment.com',
411
- 'username': '',
412
- 'password': '',
413
- 'status': 'draft'
414
- }
415
 
416
- # Lock/Unlock toggle
417
- col_lock, col_spacer = st.columns([1, 4])
418
- with col_lock:
419
- st.session_state.wp_config_locked = st.toggle(
420
- "🔒",
421
- value=st.session_state.wp_config_locked,
422
- help="Lock/Unlock configuration"
423
- )
424
-
425
- # WordPress configuration
426
- wp_url = st.text_input(
427
- "WordPress URL",
428
- value=current_config['url'],
429
- help="Your WordPress site URL",
430
- disabled=st.session_state.wp_config_locked
431
- )
432
-
433
- wp_username = st.text_input(
434
- "Username",
435
- value=current_config['username'],
436
- placeholder="admin",
437
- help="WordPress username",
438
- disabled=st.session_state.wp_config_locked
439
- )
440
 
441
- wp_password = st.text_input(
442
- "Application Password",
443
- value=current_config['password'],
444
- type="password",
445
- placeholder="••••••••",
446
- help="From WordPress: Users → Profile → Application Passwords",
447
- disabled=st.session_state.wp_config_locked
448
- )
449
 
450
- # Post status
451
- post_status = st.selectbox(
452
- "Post Status",
453
- options=["draft", "publish"],
454
- index=0 if current_config.get('status') == 'draft' else 1,
455
- help="Set whether the post should be a draft or immediately published"
 
 
456
  )
457
- st.session_state.post_status = post_status
458
-
459
- # Save button
460
- if not st.session_state.wp_config_locked:
461
- if st.button("💾 Save Configuration", use_container_width=True):
462
- st.session_state.wp_config = {
463
- 'url': wp_url,
464
- 'username': wp_username,
465
- 'password': wp_password,
466
- 'status': post_status
467
- }
468
- st.success("Configuration saved!")
469
 
470
- # ========== PREVIEW SECTION ==========
471
- if st.session_state.parsed_data:
472
- st.markdown("---")
473
- st.markdown("### 👁️ Preview")
474
-
475
- parsed = st.session_state.parsed_data
476
-
477
- # Show extracted data
478
- col_pre1, col_pre2, col_pre3 = st.columns(3)
479
-
480
- with col_pre1:
481
- st.markdown("**SEO Title**")
482
- st.info(parsed['seo_title'])
483
-
484
- st.markdown("**URL Slug**")
485
- st.code(parsed['url_slug'])
486
 
487
- with col_pre2:
488
- st.markdown("**Meta Description**")
489
- st.text(parsed['meta_description'][:100] + "..." if len(parsed['meta_description']) > 100 else parsed['meta_description'])
490
-
491
- st.markdown("**Primary Keyword**")
492
- st.success(parsed['primary_keyword'])
493
-
494
- with col_pre3:
495
- st.markdown("**Tags**")
496
- tags_html = ""
497
- for tag in parsed['tags'][:4]:
498
- tags_html += f'<span style="background:#E5E7EB; padding:0.25rem 0.75rem; border-radius:12px; margin-right:0.5rem; font-size:0.9rem;">{tag}</span>'
499
- st.markdown(tags_html, unsafe_allow_html=True)
500
-
501
- st.markdown(f"**Content Length:** {len(parsed['content'])} chars")
502
 
503
- # Rank Math meta preview
504
- with st.expander("🔴 Rank Math Meta Fields (Will be set)"):
505
- for key, value in parsed['rank_math_meta'].items():
506
- st.write(f"**{key}:**")
507
- st.code(str(value)[:200] + "..." if len(str(value)) > 200 else str(value))
508
-
509
- # ========== PUBLISH SECTION ==========
510
- st.markdown("---")
511
- st.markdown("### 🚀 Ready to Publish")
512
-
513
- # Status indicators
514
- status_cols = st.columns(4)
515
-
516
- with status_cols[0]:
517
- status_content = "✅ Content" if st.session_state.parsed_data else "❌ Content"
518
- st.markdown(f"**{status_content}**")
519
-
520
- with status_cols[1]:
521
- status_image = "✅ Image" if st.session_state.uploaded_image else "⚠️ Image Optional"
522
- st.markdown(f"**{status_image}**")
523
-
524
- # Check config status using stored config or current inputs
525
- config_ready = False
526
- if st.session_state.wp_config:
527
- config_ready = all([
528
- st.session_state.wp_config.get('url'),
529
- st.session_state.wp_config.get('username'),
530
- st.session_state.wp_config.get('password')
531
- ])
532
- else:
533
- config_ready = all([wp_url, wp_username, wp_password])
534
-
535
- with status_cols[2]:
536
- status_wp = "✅ WordPress" if config_ready else "❌ WordPress"
537
- st.markdown(f"**{status_wp}**")
538
-
539
- with status_cols[3]:
540
- st.markdown(f"**📊 Status: {post_status.upper()}**")
541
-
542
- # Main publish button
543
- publish_disabled = not (st.session_state.parsed_data and config_ready)
544
-
545
- if st.button(
546
- "🚀 PUSH TO WORDPRESS",
547
- disabled=publish_disabled,
548
- key="publish_button",
549
- use_container_width=True
550
- ):
551
- # Show progress
552
- progress_bar = st.progress(0)
553
- status_text = st.empty()
554
 
555
  try:
556
- # Use stored config or create from current inputs
557
- if st.session_state.wp_config and all([
558
- st.session_state.wp_config.get('url'),
559
- st.session_state.wp_config.get('username'),
560
- st.session_state.wp_config.get('password')
561
- ]):
562
- # Use existing stored config
563
- pass
564
- else:
565
- # Save new config from current inputs
566
- st.session_state.wp_config = {
567
- 'url': wp_url.rstrip('/'),
568
- 'username': wp_username,
569
- 'password': wp_password
570
- }
571
 
572
  # Step 1: Upload image
573
  media_id = None
574
- if uploaded_image:
575
- status_text.text("🖼️ Uploading image to WordPress...")
576
  media_id = upload_image_to_wordpress(
577
- uploaded_image,
578
- st.session_state.wp_config,
579
- st.session_state.parsed_data['url_slug']
580
  )
581
- progress_bar.progress(40)
 
582
 
583
  # Step 2: Create post
584
- status_text.text("📤 Creating post with Rank Math meta...")
585
  result = create_wordpress_post(
586
- st.session_state.parsed_data,
587
- st.session_state.wp_config,
588
- media_id
 
589
  )
590
- progress_bar.progress(80)
591
-
592
- # Step 3: Complete
593
- progress_bar.progress(100)
594
- status_text.text("✅ Complete!")
595
 
596
  if result:
597
- # Success
598
- st.balloons()
599
- st.success(f"✅ Post published successfully! (ID: {result['id']})")
600
-
601
- # Show results
602
- col_result = st.columns(2)
603
-
604
- with col_result[0]:
605
- st.metric("Post ID", result['id'])
606
- st.metric("Status", result['status'])
607
-
608
- with col_result[1]:
609
- st.metric("Date", result['date'][:10])
610
- st.link_button("View Post", result['link'])
611
-
612
- # Export data
613
  export_data = {
614
  'post_id': result['id'],
615
  'title': result['title']['rendered'],
@@ -619,27 +507,493 @@ if st.button(
619
  'published_at': datetime.now().isoformat()
620
  }
621
 
622
- st.download_button(
623
- "📥 Download Post Data",
624
- data=json.dumps(export_data, indent=2),
625
- file_name=f"post_{result['id']}.json",
626
- mime="application/json",
627
- use_container_width=True
628
- )
629
 
630
- # Clear button for new post
631
- if st.button("🔄 Create Another Post", use_container_width=True):
632
- st.session_state.parsed_data = None
633
- st.session_state.uploaded_image = None
634
- st.rerun()
635
-
 
 
 
 
 
 
636
  else:
637
- st.error("❌ Failed to publish post")
638
 
639
  except Exception as e:
640
- st.error(f"Error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
- # ========== FOOTER ==========
643
- st.markdown("---")
644
- st.caption(f"CdGarment WordPress Publisher • {datetime.now().year}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
 
 
 
1
  import requests
2
  import re
3
  import json
 
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 {
162
+ 'url': os.getenv('WORDPRESS_URL', 'https://cdgarment.com'),
163
+ 'username': os.getenv('WORDPRESS_USERNAME', ''),
164
+ 'password': os.getenv('WORDPRESS_APP_PASSWORD', ''),
165
+ 'status': os.getenv('DEFAULT_STATUS', 'draft')
166
+ }
 
 
 
167
  except Exception as e:
168
+ print(f"Could not load rank_cd.env: {str(e)}")
169
+ return {
170
+ 'url': 'https://cdgarment.com',
171
+ 'username': '',
172
+ 'password': '',
173
+ 'status': 'draft'
174
+ }
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'],
271
+ 'rank_math_robots': ['index'] if post_status == 'publish' else ['noindex'],
272
  'rank_math_news_sitemap_robots': 'index',
273
  'rank_math_facebook_title': data['seo_title'],
274
  'rank_math_facebook_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):
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}"
306
 
307
  # Prepare image data
308
  files = {
309
+ 'file': (filename, image_data, f'image/{file_extension}')
310
  }
311
 
312
  auth = (wp_config['username'], wp_config['password'])
 
334
 
335
  return media_data['id']
336
  else:
 
337
  return None
338
 
339
  except Exception as e:
340
+ print(f"Error uploading image: {str(e)}")
341
  return None
342
 
343
+ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='draft'):
344
  """Create WordPress post with Rank Math meta"""
345
  try:
346
  # Base post data
 
348
  'title': parsed_data['seo_title'],
349
  'content': parsed_data['content'],
350
  'slug': parsed_data['url_slug'],
351
+ 'status': post_status,
352
  'meta': parsed_data['rank_math_meta']
353
  }
354
 
 
405
  return None
406
 
407
  except Exception as e:
408
+ print(f"Error creating post: {str(e)}")
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"""
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:
466
+ wp_config = {
467
+ 'url': wp_url.rstrip('/') if wp_url else '',
468
+ 'username': wp_username,
469
+ 'password': wp_password,
470
+ 'status': post_status
471
+ }
 
 
 
 
 
 
 
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  # Step 1: Upload image
481
  media_id = None
482
+ if image_file:
 
483
  media_id = upload_image_to_wordpress(
484
+ image_file,
485
+ wp_config,
486
+ parsed_data['url_slug']
487
  )
488
+
489
+ yield "📤 Creating post with Rank Math meta...", None
490
 
491
  # Step 2: Create post
 
492
  result = create_wordpress_post(
493
+ parsed_data,
494
+ wp_config,
495
+ media_id,
496
+ post_status
497
  )
 
 
 
 
 
498
 
499
  if result:
500
+ # Create download data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  export_data = {
502
  'post_id': result['id'],
503
  'title': result['title']['rendered'],
 
507
  'published_at': datetime.now().isoformat()
508
  }
509
 
510
+ # Save JSON to file
511
+ json_file = f"post_{result['id']}.json"
512
+ with open(json_file, 'w') as f:
513
+ json.dump(export_data, f, indent=2)
 
 
 
514
 
515
+ success_msg = f"""
516
+ Post published successfully!
517
+
518
+ Post ID: {result['id']}
519
+ Status: {result['status']}
520
+ Date: {result['date'][:10]}
521
+ Link: {result['link']}
522
+
523
+ Click the download button to get post data.
524
+ """
525
+
526
+ yield success_msg, json_file
527
  else:
528
+ yield "❌ Failed to publish post. Check WordPress configuration and try again.", None
529
 
530
  except Exception as e:
531
+ yield f"Error: {str(e)}", None
532
+
533
+ def clear_all():
534
+ """Clear all inputs"""
535
+ return (
536
+ "", # gemini_content
537
+ None, # image
538
+ "https://cdgarment.com", # wp_url
539
+ "", # wp_username
540
+ "", # wp_password
541
+ "draft", # post_status
542
+ "", # parse_status
543
+ "", # seo_title
544
+ "", # url_slug
545
+ "", # meta_description
546
+ "", # primary_keyword
547
+ "", # tags
548
+ "", # content_length
549
+ "", # publish_status
550
+ None, # download_file
551
+ )
552
 
553
+ # ========== GRADIO UI ==========
554
+ with gr.Blocks(
555
+ title="CdGarment WordPress Publisher"
556
+ ) as demo:
557
+
558
+ # State variables
559
+ parsed_data_state = gr.State(None)
560
+ wp_config_state = gr.State(None)
561
+
562
+ # Header
563
+ gr.HTML("""
564
+ <div class="header">
565
+ <h1>🏭 CdGarment WordPress Publisher</h1>
566
+ <div class="badge">✓ Optimized for Rank Math SEO</div>
567
+ </div>
568
+ """)
569
+
570
+ # Two main columns
571
+ with gr.Row():
572
+ with gr.Column(scale=2):
573
+ with gr.Group():
574
+ gr.Markdown("### 📋 Paste Gemini Content")
575
+ gr.Markdown("Include SEO package and article")
576
+
577
+ # Example content
578
+ example_content = """SEO Toolkit: Article #20
579
+ Element Suggestion
580
+ Primary Keyword: Apparel ODM Design Services
581
+ SEO Title: The Creative Engine: Inside CdGarment's Powerful R&D Team for ODM Success
582
+ 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.
583
+ Tags: apparel ODM partner, fashion R&D team, original design manufacturer, garment prototype development, CdGarment R&D, fashion trend development
584
+ 📝 Article #20: Full Content
585
+ The Creative Engine: How CdGarment's R&D Team Powers Global ODM Success...
586
+
587
+ [Paste your complete Gemini output here]"""
588
+
589
+ # Custom Quill editor
590
+ editor_html = gr.HTML(
591
+ value=quill_template.format(
592
+ id="gemini_editor",
593
+ value=example_content
594
+ )
595
+ )
596
+
597
+ # Hidden input to capture editor content
598
+ gemini_content = gr.Textbox(visible=False)
599
+
600
+ # JavaScript to sync editor with hidden input
601
+ sync_script = gr.HTML("""
602
+ <script>
603
+ function getEditorContent() {
604
+ const quill = document.querySelector('.ql-editor').__quill;
605
+ if (quill) {
606
+ return quill.root.innerHTML;
607
+ }
608
+ return document.querySelector('.ql-editor').innerHTML;
609
+ }
610
+
611
+ // Add event listener to parse button
612
+ document.addEventListener('DOMContentLoaded', function() {
613
+ const parseBtn = document.querySelector('button:contains("Parse Content")') || document.querySelector('button:contains("🔍 Parse Content")');
614
+ if (parseBtn) {
615
+ parseBtn.addEventListener('click', function() {
616
+ const content = getEditorContent();
617
+ const hiddenInput = document.getElementById('gemini_editor');
618
+ if (hiddenInput) {
619
+ hiddenInput.value = content;
620
+ }
621
+ });
622
+ }
623
+ });
624
+ </script>
625
+ """)
626
+
627
+ with gr.Row():
628
+ parse_btn = gr.Button("🔍 Parse Content", variant="primary", scale=2)
629
+ clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1)
630
+
631
+ with gr.Column(scale=1):
632
+ # Image upload section
633
+ with gr.Group():
634
+ gr.Markdown("### 🖼️ Image & Upload")
635
+ image_input = gr.File(
636
+ label="Upload Image",
637
+ file_types=["image"],
638
+ type="filepath"
639
+ )
640
+ image_preview = gr.Image(
641
+ label="Image Preview",
642
+ height=200,
643
+ visible=False
644
+ )
645
+ image_description = gr.Textbox(
646
+ label="Image Description",
647
+ placeholder="Auto-generated description will appear here",
648
+ lines=3
649
+ )
650
+
651
+ # WordPress settings section
652
+ with gr.Group():
653
+ gr.Markdown("### ⚙️ WordPress Settings")
654
+
655
+ # Load environment variables
656
+ env_config = load_environment_variables()
657
+
658
+ wp_url = gr.Textbox(
659
+ label="WordPress URL",
660
+ value=env_config['url'],
661
+ placeholder="https://cdgarment.com"
662
+ )
663
+ wp_username = gr.Textbox(
664
+ label="Username",
665
+ value=env_config['username'],
666
+ placeholder="admin"
667
+ )
668
+ wp_password = gr.Textbox(
669
+ label="Application Password",
670
+ value=env_config['password'],
671
+ type="password",
672
+ placeholder="••••••••"
673
+ )
674
+ post_status = gr.Radio(
675
+ label="Post Status",
676
+ choices=["draft", "publish"],
677
+ value=env_config['status']
678
+ )
679
+
680
+ save_config_btn = gr.Button("💾 Save Configuration", variant="secondary")
681
+
682
+ # Parse status and preview
683
+ parse_status = gr.Textbox(label="Parse Status", visible=False)
684
+
685
+ with gr.Row(visible=False) as preview_row:
686
+ with gr.Column():
687
+ seo_title = gr.Textbox(label="SEO Title")
688
+ url_slug = gr.Textbox(label="URL Slug")
689
+
690
+ with gr.Column():
691
+ meta_description = gr.Textbox(label="Meta Description")
692
+ primary_keyword = gr.Textbox(label="Primary Keyword")
693
+
694
+ with gr.Column():
695
+ tags = gr.Textbox(label="Tags")
696
+ content_length = gr.Textbox(label="Content Length")
697
+
698
+ # Publish section
699
+ gr.Markdown("---")
700
+ gr.Markdown("### 🚀 Ready to Publish")
701
+
702
+ # Status indicators
703
+ with gr.Row():
704
+ content_status = gr.HTML("<span class='status-indicator warning'>❌ Content</span>")
705
+ image_status = gr.HTML("<span class='status-indicator warning'>⚠️ Image Optional</span>")
706
+ wp_status = gr.HTML("<span class='status-indicator warning'>❌ WordPress</span>")
707
+ status_indicator = gr.HTML("<span class='status-indicator'>📊 Status: DRAFT</span>")
708
+
709
+ publish_btn = gr.Button("🚀 PUSH TO WORDPRESS", variant="primary", size="lg")
710
+
711
+ # Publish results
712
+ publish_status = gr.Textbox(label="Publish Status", lines=5)
713
+ download_file = gr.File(label="Download Post Data", visible=False)
714
+
715
+ # Footer
716
+ gr.Markdown("---")
717
+ gr.Markdown(f"CdGarment WordPress Publisher • {datetime.now().year}")
718
+
719
+ # ========== EVENT HANDLERS ==========
720
+
721
+ def update_status_indicators(has_content, has_image, has_wp_config, post_status_value):
722
+ """Update status indicators"""
723
+ content_html = f"<span class='status-indicator success'>✅ Content</span>" if has_content else f"<span class='status-indicator error'>❌ Content</span>"
724
+ image_html = f"<span class='status-indicator success'>✅ Image</span>" if has_image else f"<span class='status-indicator warning'>⚠️ Image Optional</span>"
725
+ wp_html = f"<span class='status-indicator success'>✅ WordPress</span>" if has_wp_config else f"<span class='status-indicator error'>❌ WordPress</span>"
726
+ status_html = f"<span class='status-indicator'>📊 Status: {post_status_value.upper()}</span>"
727
+
728
+ return content_html, image_html, wp_html, status_html
729
+
730
+ def check_wp_config(wp_url_val, wp_username_val, wp_password_val):
731
+ """Check if WordPress config is complete"""
732
+ return all([wp_url_val, wp_username_val, wp_password_val])
733
+
734
+ # Image upload handler
735
+ def update_image_preview(image_file):
736
+ if image_file:
737
+ return gr.update(visible=True, value=image_file), True
738
+ return gr.update(visible=False), False
739
+
740
+ # Parse button handler
741
+ def on_parse_click(html_content, post_status_val):
742
+ """Handle parse button click with Quill HTML input"""
743
+ if not html_content or not html_content.strip():
744
+ return (
745
+ gr.update(value="Please paste content first", visible=True),
746
+ gr.update(visible=False),
747
+ "", "", "", "", "",
748
+ None,
749
+ *update_status_indicators(False, False, False, post_status_val)
750
+ )
751
+
752
+ result, parsed = parse_content(html_content, post_status_val)
753
+
754
+ if isinstance(result, tuple):
755
+ parse_status_val, seo_title_val, url_slug_val, meta_desc_val, keyword_val, tags_val, content_len_val = result[:7]
756
+
757
+ return (
758
+ gr.update(value=parse_status_val, visible=True),
759
+ gr.update(visible=True),
760
+ seo_title_val,
761
+ url_slug_val,
762
+ meta_desc_val,
763
+ keyword_val,
764
+ tags_val,
765
+ content_len_val,
766
+ parsed,
767
+ *update_status_indicators(True, False, False, post_status_val)
768
+ )
769
+ else:
770
+ return (
771
+ gr.update(value=result, visible=True),
772
+ gr.update(visible=False),
773
+ "", "", "", "", "",
774
+ None,
775
+ *update_status_indicators(False, False, False, post_status_val)
776
+ )
777
+
778
+ # Save config handler
779
+ def on_save_config(wp_url_val, wp_username_val, wp_password_val, post_status_val):
780
+ if all([wp_url_val, wp_username_val, wp_password_val]):
781
+ wp_config = {
782
+ 'url': wp_url_val.rstrip('/'),
783
+ 'username': wp_username_val,
784
+ 'password': wp_password_val,
785
+ 'status': post_status_val
786
+ }
787
+ return (
788
+ gr.update(value="✅ Configuration saved!"),
789
+ wp_config,
790
+ *update_status_indicators(False, False, True, post_status_val)
791
+ )
792
+ return (
793
+ gr.update(value="⚠️ Please fill all WordPress configuration fields"),
794
+ None,
795
+ *update_status_indicators(False, False, False, post_status_val)
796
+ )
797
+
798
+ # Publish button handler
799
+ def on_publish_click(parsed_data, wp_config, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val):
800
+ if not parsed_data:
801
+ yield (
802
+ gr.update(value="❌ No parsed content available. Please parse content first"),
803
+ gr.update(visible=False),
804
+ *update_status_indicators(False, bool(image_file), bool(wp_config), post_status_val)
805
+ )
806
+ return
807
+
808
+ wp_config_to_use = wp_config or {
809
+ 'url': wp_url_val.rstrip('/') if wp_url_val else '',
810
+ 'username': wp_username_val,
811
+ 'password': wp_password_val,
812
+ 'status': post_status_val
813
+ }
814
+
815
+ # Update status during processing
816
+ yield (
817
+ gr.update(value="🖼️ Uploading image to WordPress..."),
818
+ gr.update(visible=False),
819
+ *update_status_indicators(True, bool(image_file), True, post_status_val)
820
+ )
821
+
822
+ # Simulate progress (Gradio doesn't have built-in progress bars for streaming)
823
+ import time
824
+ time.sleep(1)
825
+
826
+ # Call the publish function
827
+ 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))
828
+
829
+ if len(publish_result) > 1:
830
+ status_msg, download_file_val = publish_result[-1]
831
+ if download_file_val:
832
+ yield (
833
+ gr.update(value=status_msg),
834
+ gr.update(value=download_file_val, visible=True),
835
+ *update_status_indicators(True, bool(image_file), True, post_status_val)
836
+ )
837
+ else:
838
+ yield (
839
+ gr.update(value=status_msg),
840
+ gr.update(visible=False),
841
+ *update_status_indicators(True, bool(image_file), True, post_status_val)
842
+ )
843
+
844
+ # Update status indicators when inputs change
845
+ def update_indicators_on_change(gemini_content_val, image_file, wp_url_val, wp_username_val, wp_password_val, post_status_val, parsed_data):
846
+ has_content = bool(parsed_data) or bool(gemini_content_val and gemini_content_val.strip())
847
+ has_image = bool(image_file)
848
+ has_wp = check_wp_config(wp_url_val, wp_username_val, wp_password_val)
849
+ return update_status_indicators(has_content, has_image, has_wp, post_status_val)
850
+
851
+ # ========== BIND EVENT HANDLERS ==========
852
+
853
+ # Image upload
854
+ image_input.change(
855
+ update_image_preview,
856
+ inputs=[image_input],
857
+ outputs=[image_preview, image_status]
858
+ )
859
+
860
+ # Parse button
861
+ parse_btn.click(
862
+ on_parse_click,
863
+ inputs=[gemini_content, post_status],
864
+ outputs=[
865
+ parse_status,
866
+ preview_row,
867
+ seo_title,
868
+ url_slug,
869
+ meta_description,
870
+ primary_keyword,
871
+ tags,
872
+ content_length,
873
+ parsed_data_state,
874
+ content_status,
875
+ image_status,
876
+ wp_status,
877
+ status_indicator
878
+ ]
879
+ )
880
+
881
+ # Save config button
882
+ save_config_btn.click(
883
+ on_save_config,
884
+ inputs=[wp_url, wp_username, wp_password, post_status],
885
+ outputs=[parse_status, wp_config_state, content_status, image_status, wp_status, status_indicator]
886
+ )
887
+
888
+ # Publish button
889
+ publish_btn.click(
890
+ on_publish_click,
891
+ inputs=[parsed_data_state, wp_config_state, image_input, post_status, wp_url, wp_username, wp_password],
892
+ outputs=[publish_status, download_file, content_status, image_status, wp_status, status_indicator]
893
+ )
894
+
895
+ # Clear button
896
+ clear_btn.click(
897
+ clear_all,
898
+ outputs=[
899
+ gemini_content,
900
+ image_input,
901
+ wp_url,
902
+ wp_username,
903
+ wp_password,
904
+ post_status,
905
+ parse_status,
906
+ seo_title,
907
+ url_slug,
908
+ meta_description,
909
+ primary_keyword,
910
+ tags,
911
+ content_length,
912
+ publish_status,
913
+ download_file
914
+ ]
915
+ )
916
+
917
+ # Update status indicators on input changes
918
+ for input_component in [gemini_content, image_input, wp_url, wp_username, wp_password, post_status]:
919
+ input_component.change(
920
+ update_indicators_on_change,
921
+ inputs=[gemini_content, image_input, wp_url, wp_username, wp_password, post_status, parsed_data_state],
922
+ outputs=[content_status, image_status, wp_status, status_indicator]
923
+ )
924
+
925
+ # Launch the Gradio app
926
+ if __name__ == "__main__":
927
+ demo.launch(
928
+ server_name="0.0.0.0",
929
+ server_port=7861,
930
+ share=True,
931
+ debug=True,
932
+ theme=gr.themes.Soft(
933
+ primary_hue="purple",
934
+ secondary_hue="green",
935
+ ),
936
+ css="""
937
+ .gradio-container {
938
+ max-width: 1400px !important;
939
+ }
940
+ .container {
941
+ padding: 20px;
942
+ }
943
+ .header {
944
+ text-align: center;
945
+ margin-bottom: 20px;
946
+ }
947
+ .header h1 {
948
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
949
+ -webkit-background-clip: text;
950
+ -webkit-text-fill-color: transparent;
951
+ font-size: 2.5rem;
952
+ font-weight: 800;
953
+ margin-bottom: 10px;
954
+ }
955
+ .badge {
956
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
957
+ color: #991B1B;
958
+ padding: 8px 16px;
959
+ border-radius: 20px;
960
+ font-weight: 600;
961
+ display: inline-block;
962
+ margin-bottom: 20px;
963
+ }
964
+ .paste-box {
965
+ border: 2px solid #3B82F6;
966
+ border-radius: 10px;
967
+ padding: 20px;
968
+ background-color: #F8FAFC;
969
+ min-height: 500px;
970
+ }
971
+ .preview-box {
972
+ background-color: #f8f9fa;
973
+ border-radius: 10px;
974
+ padding: 15px;
975
+ margin: 10px 0;
976
+ }
977
+ .status-indicator {
978
+ display: inline-block;
979
+ padding: 5px 10px;
980
+ border-radius: 5px;
981
+ font-weight: bold;
982
+ margin: 2px;
983
+ }
984
+ .success {
985
+ background-color: #d1fae5;
986
+ color: #065f46;
987
+ }
988
+ .warning {
989
+ background-color: #fef3c7;
990
+ color: #92400e;
991
+ }
992
+ .error {
993
+ background-color: #fee2e2;
994
+ color: #991b1b;
995
+ }
996
+ """,
997
+ ssr_mode=False
998
+ )
999