Spaces:
Sleeping
Sleeping
| from shiny import reactive, render, ui | |
| import os | |
| import requests | |
| from dotenv import load_dotenv | |
| from bs4 import BeautifulSoup | |
| import time | |
| from llm_connect import get_response | |
| # Load env | |
| load_dotenv() | |
| # ===== Threads config ===== | |
| THREADS_TOKEN = os.getenv("THREADS_ACCESS_TOKEN") # <-- set this in your env | |
| # Store latest generated post text | |
| generated_threads_post = reactive.Value("") # keeping the same name so your UI doesn't change | |
| # ---- Helpers ---- | |
| def scrape_shopify_blog(url: str) -> str: | |
| try: | |
| resp = requests.get(url, timeout=10) | |
| resp.raise_for_status() | |
| soup = BeautifulSoup(resp.content, 'html.parser') | |
| # Adjust this selector to match your Shopify theme if needed | |
| section = soup.find('div', class_='w940 align-center size-content') | |
| return section.get_text(strip=True, separator=' ') if section else "" | |
| except Exception as e: | |
| return f"β Error scraping blog: {e}" | |
| def generate_threads_post( # keeping the function name to avoid UI refactor | |
| topic: str = "", | |
| url: str = "", | |
| min_len: int = 100, | |
| max_len: int = 450 | |
| ) -> str: | |
| """ | |
| Generates post text using your existing LLM helper. | |
| (Text is suitable for Threads now; you can tweak tone if you want.) | |
| """ | |
| if url: | |
| scraped = scrape_shopify_blog(url) | |
| if scraped.startswith("β") or not scraped.strip(): | |
| return f"β οΈ Failed to extract blog content from URL: {url}" | |
| prompt = ( | |
| "You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n" | |
| f"Write a detailed, engaging Threads post (min {min_len} chars, max {max_len} chars) summarizing the Shopify blog:\n\n" | |
| f"{scraped}\n\n" | |
| f"The post MUST include this exact URL: {url}\n" | |
| "Use a casual, friendly tone with emojis.\n" | |
| "VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n" | |
| f"Keep everything under {max_len} characters total." | |
| ) | |
| elif topic: | |
| prompt = ( | |
| "You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n" | |
| f"Write a short, engaging Threads post (min {min_len} chars, max {max_len} chars) about: '{topic}'.\n" | |
| "Use a casual, friendly tone with fun emojis.\n" | |
| "VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n" | |
| f"The entire post must be under {max_len} characters total." | |
| ) | |
| else: | |
| return "β οΈ Provide either a topic or a Shopify blog URL." | |
| # Step 1: Generate | |
| post = get_response( | |
| input=prompt, | |
| template=lambda x: x.strip(), | |
| llm="gemini", | |
| md=False, | |
| temperature=0.9, | |
| max_tokens=250 | |
| ).strip() | |
| if len(post) <= max_len: | |
| return post | |
| time.sleep(5) | |
| # Step 2: Shorten if needed | |
| shorten_prompt = ( | |
| f"Shorten this post to {max_len} characters or fewer. Keep the Shopify link ({url}) and all original hashtags:\n\n{post}" | |
| if url else | |
| f"Shorten this post to {max_len} characters or fewer, keeping the tone, emojis, and all original hashtags:\n\n{post}" | |
| ) | |
| shortened = get_response( | |
| input=shorten_prompt, | |
| template=lambda x: x.strip(), | |
| llm="gemini", | |
| md=False, | |
| temperature=0.7, | |
| max_tokens=200 | |
| ).strip() | |
| return shortened[:max_len] | |
| # ---- Threads poster (replaces the proxy) ---- | |
| def post_to_threads(message: str) -> str: | |
| """ | |
| One-shot TEXT post to Threads using your long-lived token. | |
| Uses: POST https://graph.threads.net/me/threads with text params | |
| """ | |
| if not THREADS_TOKEN: | |
| return "β Missing THREADS_USER_ACCESS_TOKEN environment variable." | |
| try: | |
| r = requests.post( | |
| "https://graph.threads.net/me/threads", | |
| headers={"Authorization": f"Bearer {THREADS_TOKEN}"}, | |
| params={ | |
| "text": message, | |
| "media_type": "TEXT", | |
| "auto_publish_text": "true", # one-call publish for text | |
| }, | |
| timeout=30, | |
| ) | |
| # Prefer JSON, fall back to raw text for debugging | |
| data = {} | |
| try: | |
| data = r.json() | |
| except Exception: | |
| data = {"raw": r.text} | |
| if r.ok and isinstance(data, dict) and data.get("id"): | |
| return f"β Post successful! Threads Post ID: {data['id']}" | |
| return f"β Threads error ({r.status_code}): {data}" | |
| except requests.RequestException as e: | |
| return f"β Network error posting to Threads: {e}" | |
| except Exception as e: | |
| return f"β Unexpected error: {e}" | |
| # ---- Shiny server wiring (IDs unchanged) ---- | |
| def server(input, output, session): | |
| post_status = reactive.Value("") | |
| def threads_post_draft(): | |
| topic = input.threads_topic().strip() | |
| url = input.threads_url().strip() | |
| if not topic and not url: | |
| return ui.HTML("<p><strong>β οΈ Enter a topic or a blog URL.</strong></p>") | |
| post = generate_threads_post(topic=topic, url=url) | |
| if post.startswith(("β οΈ", "β")): | |
| return ui.HTML(f"<p><strong>{post}</strong></p>") | |
| generated_threads_post.set(post) | |
| return ui.HTML(f"<p><strong>Generated Threads Post:</strong><br>{post}</p>") | |
| def threads_post_status(): | |
| return post_status() | |
| def _(): | |
| post = generated_threads_post() | |
| if not post: | |
| post_status.set("β οΈ No post generated yet.") | |
| else: | |
| post_status.set(post_to_threads(post)) | |