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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +432 -673
app.py CHANGED
@@ -1,49 +1,120 @@
 
1
  import requests
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 {
31
- 'url': os.getenv('WORDPRESS_URL', 'https://cdgarment.com'),
32
- 'username': os.getenv('WORDPRESS_USERNAME', ''),
33
- 'password': os.getenv('WORDPRESS_APP_PASSWORD', ''),
34
- 'status': os.getenv('DEFAULT_STATUS', 'draft')
35
- }
 
 
 
36
  except Exception as e:
37
- print(f"Could not load rank_cd.env: {str(e)}")
38
- return {
39
- 'url': 'https://cdgarment.com',
40
- 'username': '',
41
- 'password': '',
42
- 'status': 'draft'
43
- }
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': '',
@@ -57,66 +128,35 @@ def parse_gemini_content(content, post_status='draft'):
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
105
- r'##\s+(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)' # After heading
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
 
121
  # Generate URL slug
122
  if data['seo_title']:
@@ -129,11 +169,11 @@ def parse_gemini_content(content, post_status='draft'):
129
  if not data['seo_title'] and data['article_title']:
130
  data['seo_title'] = data['article_title']
131
 
132
- # Prepare Rank Math meta fields
133
  data['rank_math_meta'] = {
134
  'rank_math_title': data['seo_title'],
135
  'rank_math_description': data['meta_description'],
136
- 'rank_math_robots': ['index'] if post_status == 'publish' else ['noindex'],
137
  'rank_math_news_sitemap_robots': 'index',
138
  'rank_math_facebook_title': data['seo_title'],
139
  'rank_math_facebook_description': data['meta_description'],
@@ -142,35 +182,26 @@ def parse_gemini_content(content, post_status='draft'):
142
  'rank_math_canonical_url': '',
143
  }
144
 
145
- # Add primary keyword if available
146
  if data['primary_keyword']:
147
  data['rank_math_meta']['rank_math_focus_keyword'] = data['primary_keyword']
148
 
149
  return data
150
 
151
  except Exception as e:
152
- print(f"Error parsing: {str(e)}")
153
- return {'error': f"Error parsing: {str(e)}"}
154
 
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}"
170
 
171
  # Prepare image data
172
  files = {
173
- 'file': (filename, image_data, f'image/{file_extension}')
174
  }
175
 
176
  auth = (wp_config['username'], wp_config['password'])
@@ -198,14 +229,14 @@ def upload_image_to_wordpress(image_file, wp_config, filename_slug):
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:
205
- print(f"Error uploading image: {str(e)}")
206
  return None
207
 
208
- def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='draft'):
209
  """Create WordPress post with Rank Math meta"""
210
  try:
211
  # Base post data
@@ -213,7 +244,7 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='dr
213
  'title': parsed_data['seo_title'],
214
  'content': parsed_data['content'],
215
  'slug': parsed_data['url_slug'],
216
- 'status': post_status,
217
  'meta': parsed_data['rank_math_meta']
218
  }
219
 
@@ -267,99 +298,318 @@ def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='dr
267
 
268
  return post_result
269
  else:
270
- print(f"Post creation failed: {response.text}")
271
  return None
272
 
273
  except Exception as e:
274
- print(f"Error creating post: {str(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'], "", "", "", "", "", "", {}
 
 
 
 
 
 
 
 
 
 
 
287
 
288
- # Ensure all fields exist
289
- parsed.setdefault('seo_title', 'No title')
290
- parsed.setdefault('meta_description', '')
291
- parsed.setdefault('primary_keyword', '')
292
- parsed.setdefault('tags', [])
293
- parsed.setdefault('content', '')
294
- parsed.setdefault('url_slug', '')
295
 
296
- # Format tags for display
297
- if parsed['tags'] and len(parsed['tags']) > 0:
298
- tags_display = ", ".join(parsed['tags'][:8])
299
- else:
300
- tags_display = "No tags found in content"
 
 
301
 
302
- # Format meta description preview
303
- if parsed['meta_description']:
304
- meta_preview = parsed['meta_description'][:120] + "..." if len(parsed['meta_description']) > 120 else parsed['meta_description']
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:
327
- wp_config = {
328
- 'url': wp_url.rstrip('/') if wp_url else '',
329
- 'username': wp_username,
330
- 'password': wp_password,
331
- 'status': post_status
332
- }
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
  # Step 1: Upload image
343
  media_id = None
344
- if image_file:
 
345
  media_id = upload_image_to_wordpress(
346
- image_file,
347
- wp_config,
348
- parsed_data['url_slug']
349
  )
350
-
351
- yield "📤 Creating post with Rank Math meta...", None
352
 
353
  # Step 2: Create post
 
354
  result = create_wordpress_post(
355
- parsed_data,
356
- wp_config,
357
- media_id,
358
- post_status
359
  )
 
 
 
 
 
360
 
361
  if result:
362
- # Create download data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  export_data = {
364
  'post_id': result['id'],
365
  'title': result['title']['rendered'],
@@ -369,518 +619,27 @@ def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_use
369
  'published_at': datetime.now().isoformat()
370
  }
371
 
372
- # Save JSON to file
373
- json_file = f"post_{result['id']}.json"
374
- with open(json_file, 'w') as f:
375
- json.dump(export_data, f, indent=2)
376
-
377
- success_msg = f"""
378
- ✅ Post published successfully!
379
-
380
- Post ID: {result['id']}
381
- Status: {result['status']}
382
- Date: {result['date'][:10]}
383
- Link: {result['link']}
384
-
385
- Click the download button to get post data.
386
- """
387
 
388
- yield success_msg, json_file
 
 
 
 
 
389
  else:
390
- yield "❌ Failed to publish post. Check WordPress configuration and try again.", None
391
 
392
  except Exception as e:
393
- yield f"Error: {str(e)}", None
394
-
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
402
- "", # wp_password
403
- "draft", # post_status
404
- "", # parse_status
405
- "", # seo_title
406
- "", # url_slug
407
- "", # meta_description
408
- "", # primary_keyword
409
- "", # tags
410
- "", # content_length
411
- "", # publish_status
412
- None, # download_file
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)
420
- wp_config_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" data-wrap="# " data-type="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" data-wrap="## " data-type="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" data-wrap="**" data-type="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" data-wrap="*" data-type="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" data-wrap="- " data-type="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('data-wrap');
492
- const type = this.getAttribute('data-type');
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
572
- with gr.Group():
573
- gr.Markdown("### 🖼️ Image & Upload")
574
- image_input = gr.File(
575
- label="Upload Image",
576
- file_types=["image"],
577
- type="filepath"
578
- )
579
- image_preview = gr.Image(
580
- label="Image Preview",
581
- height=200,
582
- visible=False
583
- )
584
- image_description = gr.Textbox(
585
- label="Image Description",
586
- placeholder="Auto-generated description will appear here",
587
- lines=3
588
- )
589
-
590
- # WordPress settings section
591
- with gr.Group():
592
- gr.Markdown("### ⚙️ WordPress Settings")
593
-
594
- # Load environment variables
595
- env_config = load_environment_variables()
596
-
597
- wp_url = gr.Textbox(
598
- label="WordPress URL",
599
- value=env_config['url'],
600
- placeholder="https://cdgarment.com"
601
- )
602
- wp_username = gr.Textbox(
603
- label="Username",
604
- value=env_config['username'],
605
- placeholder="admin"
606
- )
607
- wp_password = gr.Textbox(
608
- label="Application Password",
609
- value=env_config['password'],
610
- type="password",
611
- placeholder="••••••••"
612
- )
613
- post_status = gr.Radio(
614
- label="Post Status",
615
- choices=["draft", "publish"],
616
- value=env_config['status']
617
- )
618
-
619
- save_config_btn = gr.Button("💾 Save Configuration", variant="secondary")
620
-
621
- # Parse status and preview
622
- parse_status = gr.Textbox(label="Parse Status", visible=False)
623
-
624
- with gr.Row(visible=False) as preview_row:
625
- with gr.Column():
626
- seo_title = gr.Textbox(label="SEO Title")
627
- url_slug = gr.Textbox(label="URL Slug")
628
-
629
- with gr.Column():
630
- meta_description = gr.Textbox(label="Meta Description")
631
- primary_keyword = gr.Textbox(label="Primary Keyword")
632
-
633
- with gr.Column():
634
- tags = gr.Textbox(label="Tags")
635
- content_length = gr.Textbox(label="Content Length")
636
-
637
- # Publish section
638
- gr.Markdown("---")
639
- gr.Markdown("### 🚀 Ready to Publish")
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
-
650
- # Publish results
651
- publish_status = gr.Textbox(label="Publish Status", lines=5)
652
- download_file = gr.File(label="Download Post Data", visible=False)
653
-
654
- # Footer
655
- gr.Markdown("---")
656
- gr.Markdown(f"CdGarment WordPress Publisher • {datetime.now().year}")
657
-
658
- # ========== EVENT HANDLERS ==========
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
-
678
- def check_wp_config(wp_url_val, wp_username_val, wp_password_val):
679
- """Check if WordPress config is complete"""
680
- return all([wp_url_val, wp_username_val, wp_password_val])
681
-
682
- # Image upload handler
683
- def update_image_preview(image_file):
684
- if image_file:
685
- return gr.update(visible=True, value=image_file), True
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),
694
- "", "", "", "", "",
695
- None,
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),
711
- seo_title_val,
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)
719
- )
720
- else:
721
- return (
722
- gr.update(value=result, visible=True),
723
- gr.update(visible=False),
724
- "", "", "", "", "",
725
- None,
726
- *update_status_indicators(False, False, False, post_status_val)
727
- )
728
-
729
- # Save config handler
730
- def on_save_config(wp_url_val, wp_username_val, wp_password_val, post_status_val):
731
- if all([wp_url_val, wp_username_val, wp_password_val]):
732
- wp_config = {
733
- 'url': wp_url_val.rstrip('/'),
734
- 'username': wp_username_val,
735
- 'password': wp_password_val,
736
- 'status': post_status_val
737
- }
738
- return (
739
- gr.update(value="✅ Configuration saved!"),
740
- wp_config,
741
- *update_status_indicators(False, False, True, post_status_val)
742
- )
743
- return (
744
- gr.update(value="⚠️ Please fill all WordPress configuration fields"),
745
- None,
746
- *update_status_indicators(False, False, False, post_status_val)
747
- )
748
-
749
- # Publish button handler
750
- def on_publish_click(parsed_data, wp_config, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val):
751
- if not parsed_data:
752
- yield (
753
- gr.update(value="❌ No parsed content available. Please parse content first"),
754
- gr.update(visible=False),
755
- *update_status_indicators(False, bool(image_file), bool(wp_config), post_status_val)
756
- )
757
- return
758
-
759
- wp_config_to_use = wp_config or {
760
- 'url': wp_url_val.rstrip('/') if wp_url_val else '',
761
- 'username': wp_username_val,
762
- 'password': wp_password_val,
763
- 'status': post_status_val
764
- }
765
-
766
- # Update status during processing
767
- yield (
768
- gr.update(value="🖼️ Uploading image to WordPress..."),
769
- gr.update(visible=False),
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),
778
- gr.update(value=download_file_val, visible=True),
779
- *update_status_indicators(True, bool(image_file), True, post_status_val)
780
- )
781
- else:
782
- yield (
783
- 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)
795
-
796
- # ========== BIND EVENT HANDLERS ==========
797
-
798
- # Image upload
799
- image_input.change(
800
- update_image_preview,
801
- inputs=[image_input],
802
- outputs=[image_preview, image_status]
803
- )
804
-
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,
812
- seo_title,
813
- url_slug,
814
- meta_description,
815
- primary_keyword,
816
- tags,
817
- content_length,
818
- parsed_data_state,
819
- content_status,
820
- image_status,
821
- wp_status,
822
- status_indicator
823
- ]
824
- )
825
-
826
- # Save config button
827
- save_config_btn.click(
828
- on_save_config,
829
- inputs=[wp_url, wp_username, wp_password, post_status],
830
- outputs=[parse_status, wp_config_state, content_status, image_status, wp_status, status_indicator]
831
- )
832
-
833
- # Publish button
834
- publish_btn.click(
835
- on_publish_click,
836
- inputs=[parsed_data_state, wp_config_state, image_input, post_status, wp_url, wp_username, wp_password],
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,
848
- wp_password,
849
- post_status,
850
- parse_status,
851
- seo_title,
852
- url_slug,
853
- meta_description,
854
- primary_keyword,
855
- tags,
856
- content_length,
857
- publish_status,
858
- download_file
859
- ]
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=7860,
875
- share=False,
876
- debug=True,
877
- ssr_mode=False,
878
- css="""
879
- .gradio-container { max-width: 1400px !important; }
880
- .paste-box textarea {
881
- font-family: 'Courier New', monospace !important;
882
- font-size: 14px !important;
883
- line-height: 1.5 !important;
884
- }
885
- """
886
- )
 
1
+ import streamlit as st
2
  import requests
3
  import re
4
  import json
5
  from datetime import datetime
6
+ 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': '',
 
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
  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
  '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
 
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
  '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
 
 
298
 
299
  return post_result
300
  else:
 
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
  '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