Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| import random | |
| import re | |
| import time | |
| import html as html_lib | |
| import requests | |
| from datetime import datetime | |
| # ============================================================ | |
| # PAGE CONFIGURATION | |
| # ============================================================ | |
| st.set_page_config( | |
| page_title="LinkedIn Translator | Normal English → LinkedIn Language", | |
| page_icon="💼", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| # ============================================================ | |
| # SESSION STATE INITIALIZATION | |
| # ============================================================ | |
| if "history" not in st.session_state: | |
| st.session_state.history = [] | |
| if "theme" not in st.session_state: | |
| st.session_state.theme = "dark" | |
| if "translation_count" not in st.session_state: | |
| st.session_state.translation_count = 0 | |
| if "last_output" not in st.session_state: | |
| st.session_state.last_output = "" | |
| if "last_input" not in st.session_state: | |
| st.session_state.last_input = "" | |
| if "last_style" not in st.session_state: | |
| st.session_state.last_style = "" | |
| if "was_spun" not in st.session_state: | |
| st.session_state.was_spun = False | |
| if "last_ai_used" not in st.session_state: | |
| st.session_state.last_ai_used = False | |
| # ============================================================ | |
| # STYLE DEFINITIONS | |
| # ============================================================ | |
| STYLES = { | |
| "Corporate Buzzword 🏢": { | |
| "key": "corporate", | |
| "desc": "Synergy, leverage, and moving the needle. Sound important while saying nothing.", | |
| "color": "#0A66C2", | |
| }, | |
| "Humble Brag 😌": { | |
| "key": "humblebrag", | |
| "desc": "Humbled. Honored. Blessed. Modesty is just another word for showing off.", | |
| "color": "#E7A33E", | |
| }, | |
| "Hustle Culture 💪": { | |
| "key": "hustle", | |
| "desc": "5AM wakeups, no days off, and questionable life choices.", | |
| "color": "#FF6B35", | |
| }, | |
| "Thought Leader 🧠": { | |
| "key": "thoughtleader", | |
| "desc": "Hot takes nobody asked for. Let that sink in.", | |
| "color": "#7B68EE", | |
| }, | |
| "Tech Bro 🚀": { | |
| "key": "techbro", | |
| "desc": "Shipping, iterating, and disrupting. What's your stack?", | |
| "color": "#00D4AA", | |
| }, | |
| "Motivational Speaker 🔥": { | |
| "key": "motivational", | |
| "desc": "REMINDER: You are a warrior. Act like it.", | |
| "color": "#FF4500", | |
| }, | |
| "Maximum Cringe 😬": { | |
| "key": "maxcringe", | |
| "desc": "All LinkedIn stereotypes combined. You've been warned.", | |
| "color": "#FF1493", | |
| }, | |
| } | |
| EXAMPLE_INPUTS = [ | |
| "I got a new job", | |
| "I lost my job", | |
| "I learned Python today", | |
| "I woke up early today", | |
| "I made a mistake at work", | |
| "We had a meeting about the project", | |
| "I had coffee this morning", | |
| "I failed my interview", | |
| ] | |
| # ============================================================ | |
| # THEMING / CSS | |
| # ============================================================ | |
| def inject_css(theme): | |
| if theme == "dark": | |
| css = """ | |
| <style> | |
| .stApp { | |
| background: linear-gradient(135deg, #0d1117 0%, #161b22 100%) !important; | |
| } | |
| .stSidebar > div:first-child { | |
| background: linear-gradient(180deg, #0d1117 0%, #1c2333 100%) !important; | |
| } | |
| .main-title { | |
| background: linear-gradient(90deg, #0A66C2, #7B68EE, #FF6B35); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 3rem !important; | |
| font-weight: 800 !important; | |
| text-align: center; | |
| margin-bottom: 0 !important; | |
| } | |
| .subtitle { | |
| color: #8b949e !important; | |
| text-align: center; | |
| font-size: 1.1rem !important; | |
| margin-bottom: 2rem !important; | |
| } | |
| .input-card, .output-card { | |
| background: #161b22; | |
| border: 1px solid #30363d; | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.4); | |
| } | |
| .input-card { border-left: 4px solid #8b949e; } | |
| .output-card { border-left: 4px solid #0A66C2; } | |
| .card-label { | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| margin-bottom: 0.75rem; | |
| } | |
| .input-label { color: #8b949e; } | |
| .output-label { color: #0A66C2; } | |
| .stat-box { | |
| background: #21262d; | |
| border: 1px solid #30363d; | |
| border-radius: 12px; | |
| padding: 0.75rem 1rem; | |
| text-align: center; | |
| } | |
| .stat-value { font-size: 1.5rem; font-weight: 800; color: #0A66C2; } | |
| .stat-label { font-size: 0.7rem; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; } | |
| .cringe-bar-bg { background: #21262d; border-radius: 10px; height: 14px; overflow: hidden; } | |
| .cringe-bar-fill { height: 100%; border-radius: 10px; transition: width 0.8s ease; } | |
| .history-item { | |
| background: #21262d; | |
| border: 1px solid #30363d; | |
| border-radius: 10px; | |
| padding: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.82rem; | |
| color: #c9d1d9; | |
| } | |
| .style-badge { | |
| display: inline-block; | |
| padding: 2px 10px; | |
| border-radius: 12px; | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| color: white; | |
| } | |
| .divider-line { border: none; height: 1px; background: #30363d; margin: 1rem 0; } | |
| .output-display { | |
| background: #0d1117; | |
| border: 1px solid #30363d; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| min-height: 150px; | |
| color: #e6edf3; | |
| font-size: 0.95rem; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| } | |
| .output-empty { | |
| background: #0d1117; | |
| border: 1px dashed #30363d; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| min-height: 150px; | |
| color: #484f58; | |
| font-size: 0.95rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| } | |
| .spin-tag { | |
| display: inline-block; | |
| background: #238636; | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .ai-tag { | |
| display: inline-block; | |
| background: #7B68EE; | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .rule-tag { | |
| display: inline-block; | |
| background: #484f58; | |
| color: #c9d1d9; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .tags-container { | |
| margin-bottom: 8px; | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| } | |
| .stTextArea > div > div > textarea { | |
| background-color: #0d1117 !important; | |
| color: #e6edf3 !important; | |
| border-color: #30363d !important; | |
| border-radius: 10px !important; | |
| } | |
| .stTextArea > div > div > textarea:focus { | |
| border-color: #0A66C2 !important; | |
| box-shadow: 0 0 0 2px rgba(10,102,194,0.3) !important; | |
| } | |
| .stButton > button { | |
| border-radius: 10px !important; | |
| font-weight: 600 !important; | |
| transition: all 0.2s !important; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 12px rgba(10,102,194,0.4) !important; | |
| } | |
| .stSelectbox > div > div { background-color: #0d1117 !important; color: #e6edf3 !important; } | |
| </style> | |
| """ | |
| else: | |
| css = """ | |
| <style> | |
| .stApp { | |
| background: linear-gradient(135deg, #f0f2f5 0%, #e8ecf1 100%) !important; | |
| } | |
| .stSidebar > div:first-child { | |
| background: linear-gradient(180deg, #ffffff 0%, #f6f8fa 100%) !important; | |
| } | |
| .main-title { | |
| background: linear-gradient(90deg, #0A66C2, #7B68EE, #FF6B35); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 3rem !important; | |
| font-weight: 800 !important; | |
| text-align: center; | |
| margin-bottom: 0 !important; | |
| } | |
| .subtitle { | |
| color: #5e5e5e !important; | |
| text-align: center; | |
| font-size: 1.1rem !important; | |
| margin-bottom: 2rem !important; | |
| } | |
| .input-card, .output-card { | |
| background: #ffffff; | |
| border: 1px solid #d0d7de; | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.08); | |
| } | |
| .input-card { border-left: 4px solid #8b949e; } | |
| .output-card { border-left: 4px solid #0A66C2; } | |
| .card-label { | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| margin-bottom: 0.75rem; | |
| } | |
| .input-label { color: #57606a; } | |
| .output-label { color: #0A66C2; } | |
| .stat-box { | |
| background: #ffffff; | |
| border: 1px solid #d0d7de; | |
| border-radius: 12px; | |
| padding: 0.75rem 1rem; | |
| text-align: center; | |
| } | |
| .stat-value { font-size: 1.5rem; font-weight: 800; color: #0A66C2; } | |
| .stat-label { font-size: 0.7rem; color: #57606a; text-transform: uppercase; letter-spacing: 1px; } | |
| .cringe-bar-bg { background: #e1e4e8; border-radius: 10px; height: 14px; overflow: hidden; } | |
| .cringe-bar-fill { height: 100%; border-radius: 10px; transition: width 0.8s ease; } | |
| .history-item { | |
| background: #ffffff; | |
| border: 1px solid #d0d7de; | |
| border-radius: 10px; | |
| padding: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.82rem; | |
| color: #24292f; | |
| } | |
| .style-badge { | |
| display: inline-block; | |
| padding: 2px 10px; | |
| border-radius: 12px; | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| color: white; | |
| } | |
| .divider-line { border: none; height: 1px; background: #d0d7de; margin: 1rem 0; } | |
| .output-display { | |
| background: #f6f8fa; | |
| border: 1px solid #d0d7de; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| min-height: 150px; | |
| color: #24292f; | |
| font-size: 0.95rem; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| } | |
| .output-empty { | |
| background: #f6f8fa; | |
| border: 1px dashed #d0d7de; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| min-height: 150px; | |
| color: #8b949e; | |
| font-size: 0.95rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| } | |
| .spin-tag { | |
| display: inline-block; | |
| background: #1a7f37; | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .ai-tag { | |
| display: inline-block; | |
| background: #7B68EE; | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .rule-tag { | |
| display: inline-block; | |
| background: #57606a; | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 700; | |
| vertical-align: middle; | |
| } | |
| .tags-container { | |
| margin-bottom: 8px; | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| } | |
| .stTextArea > div > div > textarea { | |
| background-color: #ffffff !important; | |
| color: #24292f !important; | |
| border-color: #d0d7de !important; | |
| border-radius: 10px !important; | |
| } | |
| .stTextArea > div > div > textarea:focus { | |
| border-color: #0A66C2 !important; | |
| box-shadow: 0 0 0 2px rgba(10,102,194,0.3) !important; | |
| } | |
| .stButton > button { | |
| border-radius: 10px !important; | |
| font-weight: 600 !important; | |
| transition: all 0.2s !important; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 12px rgba(10,102,194,0.3) !important; | |
| } | |
| </style> | |
| """ | |
| st.markdown(css, unsafe_allow_html=True) | |
| # ============================================================ | |
| # POSITIVITY SPIN LAYER | |
| # ============================================================ | |
| POSITIVITY_SPINS = [ | |
| (r'\bI lost my job\b', "I've embraced a new chapter in my career journey"), | |
| (r'\bI got fired\b', "I was given the freedom to pursue bigger things"), | |
| (r'\bI was fired\b', "I was given the freedom to pursue bigger things"), | |
| (r'\bI got laid off\b', "I was presented with an unexpected career pivot"), | |
| (r'\bI was laid off\b', "I was presented with an unexpected career pivot"), | |
| (r'\bI was let go\b', "I was released to explore new horizons"), | |
| (r'\bI was made redundant\b', "I was offered a clean slate to reinvent myself"), | |
| (r'\bI was terminated\b', "I was given a bold opportunity to start fresh"), | |
| (r'\bI\'m unemployed\b', "I\'m in between exciting opportunities"), | |
| (r'\bunemployed\b', "between opportunities"), | |
| (r'\bI got downsized\b', "I was given the gift of a fresh start"), | |
| (r'\bI quit my job\b', "I courageously chose to bet on myself"), | |
| (r'\bI resigned\b', "I boldly decided it was time for my next act"), | |
| (r'\bI walked out\b', "I took a bold stand for my values"), | |
| (r'\bI failed\b', "I discovered a powerful lesson"), | |
| (r'\bI failed at\b', "I learned immensely from"), | |
| (r'\bI failed my\b', "I had a growth experience with my"), | |
| (r'\bfailure\b', "stepping stone"), | |
| (r'\bI made a mistake\b', "I had a breakthrough learning moment"), | |
| (r'\bI messed up\b', "I found an unexpected growth opportunity"), | |
| (r'\bI screwed up\b', "I stumbled onto a valuable lesson"), | |
| (r'\bI was wrong\b', "I evolved my perspective"), | |
| (r'\bI ruined\b', "I transformed"), | |
| (r'\bI gave up\b', "I courageously redirected my energy"), | |
| (r'\bI was rejected\b', "I was redirected toward something better"), | |
| (r'\bI got rejected\b', "I was protected from the wrong path"), | |
| (r'\bI got turned down\b', "I was being saved for the right opportunity"), | |
| (r'\bI didn\'t get the job\b', "The right role is still finding me"), | |
| (r'\bI didn\'t get accepted\b', "I was being guided to my true path"), | |
| (r'\bI failed my interview\b', "My interview became a masterclass in growth"), | |
| (r'\brejected\b', "redirected"), | |
| (r'\bI\'m struggling\b', "I\'m in the crucible that forges future leaders"), | |
| (r'\bI\'m stuck\b', "I\'m in a season of preparation for something great"), | |
| (r'\bI\'m lost\b', "I\'m exploring uncharted territory with curiosity"), | |
| (r'\bI\'m confused\b', "I\'m navigating a beautiful period of discovery"), | |
| (r'\bI\'m overwhelmed\b', "I\'m being stretched into the leader I\'m meant to be"), | |
| (r'\bI\'m stressed\b', "I\'m being forged in the fire of purpose"), | |
| (r'\bI\'m exhausted\b', "I\'m pouring everything into what matters"), | |
| (r'\bI\'m tired\b', "I\'m investing my energy in meaningful work"), | |
| (r'\bI\'m burned out\b', "I\'m taking a well-deserved season of renewal"), | |
| (r'\bI\'m broken\b', "I\'m being rebuilt stronger than ever"), | |
| (r'\bI\'m sad\b', "I\'m processing deep emotions that make me human"), | |
| (r'\bI\'m depressed\b', "I\'m navigating a chapter that will inspire others someday"), | |
| (r'\bI\'m anxious\b', "I\'m being called to trust the process"), | |
| (r'\bI\'m lonely\b', "I\'m in a sacred season of self-discovery"), | |
| (r'\bI can\'t find a job\b', "I\'m being selective about my next career move"), | |
| (r'\bI can\'t do this\b', "I\'m being challenged to grow beyond my comfort zone"), | |
| (r'\bI can\'t\b', "I\'m still learning to"), | |
| (r'\bnothing is working\b', "I\'m in the messy middle of my breakthrough"), | |
| (r'\bnothing works\b', "every attempt brings me closer to the answer"), | |
| (r'\bI hate my job\b', "I\'m being called to find work that aligns with my purpose"), | |
| (r'\bI hate\b', "I\'m deeply motivated to transform"), | |
| (r'\bI don\'t like\b', "I\'m inspired to improve"), | |
| (r'\bI\'m broke\b', "I\'m investing in experiences over material things"), | |
| (r'\bI have no money\b', "I\'m resource-rich in ways that matter most"), | |
| (r'\bI\'m poor\b', "I\'m building wealth in character and resilience"), | |
| (r'\bI was demoted\b', "I was given the chance to reconnect with the frontline"), | |
| (r'\bI got demoted\b', "I was invited back to where the real impact happens"), | |
| (r'\bI got dumped\b', "I was freed to focus on becoming my best self"), | |
| (r'\bwe broke up\b', "we each graduated to our next chapter of growth"), | |
| (r'\bterrible\b', "challenging yet transformative"), | |
| (r'\bhorrible\b', "an invitation to grow"), | |
| (r'\bawful\b', "a catalyst for change"), | |
| (r'\bworst\b', "most defining"), | |
| (r'\bdisaster\b', "opportunity in disguise"), | |
| (r'\bcatastrophe\b', "turning point"), | |
| (r'\bnightmare\b', "defining challenge that shaped me"), | |
| (r'\bproblem\b', "opportunity"), | |
| (r'\bissue\b', "growth area"), | |
| (r'\bweakness\b', "opportunity for development"), | |
| ] | |
| def apply_positivity_spin(text): | |
| original = text | |
| for pattern, replacement in POSITIVITY_SPINS: | |
| text = re.sub(pattern, replacement, text, flags=re.IGNORECASE) | |
| was_spun = (text != original) | |
| return text, was_spun | |
| # ============================================================ | |
| # TRANSLATION ENGINE — RULE-BASED | |
| # ============================================================ | |
| def apply_replacements(text, style): | |
| universal = [ | |
| (r'\bvery good\b', 'absolutely phenomenal'), | |
| (r'\bvery bad\b', 'a tremendous growth opportunity'), | |
| (r'\bworked hard\b', 'poured my heart and soul'), | |
| (r'\bsuccessful\b', 'wildly successful'), | |
| ] | |
| for pattern, repl in universal: | |
| text = re.sub(pattern, repl, text, flags=re.IGNORECASE) | |
| all_replacements = { | |
| "corporate": [ | |
| (r'\bjob\b', 'role within the organizational ecosystem'), | |
| (r'\bwork\b', 'value-adding initiative'), | |
| (r'\bmeeting\b', 'strategic alignment session'), | |
| (r'\bhelp\b', 'provide cross-functional bandwidth for'), | |
| (r'\bidea\b', 'actionable insight with measurable ROI'), | |
| (r'\bplan\b', 'strategic roadmap with key milestones'), | |
| (r'\bproject\b', 'flagship initiative'), | |
| (r'\bteam\b', 'high-performing squad'), | |
| (r'\bboss\b', 'key stakeholder'), | |
| (r'\bchange\b', 'paradigm shift'), | |
| (r'\bthink\b', 'ideate'), | |
| (r'\buse\b', 'leverage'), | |
| (r'\bstart\b', 'kick off'), | |
| (r'\bimprove\b', 'move the needle on'), | |
| (r'\btalk\b', 'circle back on'), | |
| (r'\blearn\b', 'deep dive into'), | |
| (r'\bmake\b', 'operationalize'), | |
| (r'\bimportant\b', 'mission-critical'), | |
| (r'\bfast\b', 'with velocity and agility'), | |
| (r'\bbig\b', 'at scale'), | |
| (r'\bnew\b', 'net-new'), | |
| (r'\btogether\b', 'synergistically'), | |
| (r'\bgood\b', 'high-impact'), | |
| (r'\bdo\b', 'execute against'), | |
| (r'\bneed\b', 'have identified a requirement for'), | |
| (r'\btry\b', 'explore the feasibility of'), | |
| (r'\bsend\b', 'cascade'), | |
| (r'\bfix\b', 'remediate'), | |
| (r'\bcheck\b', 'socialize'), | |
| ], | |
| "humblebrag": [ | |
| (r'\bI got\b', 'I was humbled and honored to receive'), | |
| (r'\bI achieved\b', 'I was blessed to achieve'), | |
| (r'\bI won\b', "By God's grace, I was awarded"), | |
| (r'\bI was promoted\b', 'I was graciously entrusted with an expanded role'), | |
| (r'\bgood\b', 'beyond what I deserve'), | |
| (r'\bhired\b', 'welcomed into an incredible family'), | |
| (r'\bpromoted\b', 'given an opportunity I scarcely deserve'), | |
| (r'\baward\b', 'a recognition that left me speechless'), | |
| (r'\bexcited\b', 'humbled beyond words'), | |
| (r'\bproud\b', 'overcome with gratitude'), | |
| (r'\bhappy\b', 'blessed beyond measure'), | |
| (r'\bsuccessful\b', 'overwhelmed by the kindness of the universe'), | |
| ], | |
| "hustle": [ | |
| (r'\bwoke up\b', 'was already grinding while others were sleeping'), | |
| (r'\bworked\b', 'was putting in the work'), | |
| (r'\bsleep\b', 'sleep is for the weak'), | |
| (r'\brest\b', "rest? Never heard of her"), | |
| (r'\bjob\b', 'grind'), | |
| (r'\bwork\b', 'the hustle'), | |
| (r'\beffort\b', 'relentless dedication'), | |
| (r'\bgood\b', 'next level'), | |
| (r'\bsuccess\b', 'the result of outworking everyone'), | |
| (r'\bhard\b', 'the price of greatness'), | |
| (r'\bfailed\b', 'one step closer to the goal'), | |
| (r'\btrying\b', 'committing 110%'), | |
| (r'\bday off\b', "what's that?"), | |
| (r'\bweekend\b', 'just another opportunity to grind'), | |
| ], | |
| "thoughtleader": [ | |
| (r'\bI think\b', 'My contrarian take:'), | |
| (r'\bI believe\b', "Here's the truth nobody wants to hear:"), | |
| (r'\bI learned\b', 'What I discovered will change how you see everything:'), | |
| (r'\bI found\b', 'The insight that changed my perspective:'), | |
| (r'\bimportant\b', 'the single most overlooked truth'), | |
| (r'\bgood\b', 'what the top 1% understand'), | |
| (r'\bpeople\b', 'most people'), | |
| (r'\badvice\b', 'the uncomfortable truth'), | |
| ], | |
| "techbro": [ | |
| (r'\bmade\b', 'shipped'), | |
| (r'\bbuilt\b', 'shipped'), | |
| (r'\bcreated\b', 'shipped'), | |
| (r'\bstarted\b', 'launched my MVP'), | |
| (r'\blearned\b', 'leveled up in'), | |
| (r'\bjob\b', 'role in the trenches'), | |
| (r'\bwork\b', "what I'm building"), | |
| (r'\bproject\b', "side project that's about to be huge"), | |
| (r'\bidea\b', 'the next big thing'), | |
| (r'\bteam\b', 'my co-founders and I'), | |
| (r'\bcompany\b', 'my startup'), | |
| (r'\bmoney\b', 'runway'), | |
| (r'\bgood\b', 'absolutely cracked'), | |
| (r'\bfast\b', 'at breakneck speed'), | |
| (r'\bimprove\b', 'iterate on'), | |
| (r'\bchange\b', 'disrupt'), | |
| (r'\bplan\b', 'product roadmap'), | |
| (r'\bnew\b', 'net-new'), | |
| ], | |
| "motivational": [ | |
| (r'\bgood\b', 'UNSTOPPABLE'), | |
| (r'\bhard\b', 'THE FIRE THAT FORGES CHAMPIONS'), | |
| (r'\btired\b', 'RECHARGING FOR GREATNESS'), | |
| (r'\bscared\b', 'ABOUT TO DO SOMETHING BRAVE'), | |
| (r"\bcan't\b", "HAVEN'T YET"), | |
| (r'\bimpossible\b', 'A CHALLENGE WORTH CONQUERING'), | |
| ], | |
| "maxcringe": [ | |
| (r'\bgood\b', 'absolutely GAME-CHANGING and MIND-BLOWING'), | |
| (r'\bwork\b', 'value-adding, paradigm-shifting hustle'), | |
| (r'\bjob\b', 'purpose-driven mission'), | |
| (r'\bteam\b', 'squad of absolute ROCKSTARS'), | |
| (r'\bsuccess\b', 'WILD, UNPRECEDENTED success'), | |
| (r'\blearned\b', 'had my MIND BLOWN learning'), | |
| (r'\bexcited\b', 'BEYOND HUMBLED and literally SHAKING with excitement'), | |
| (r'\bimportant\b', 'MISSION-CRITICAL and NON-NEGOTIABLE'), | |
| (r'\bgreat\b', 'PHENOMENAL and WORLD-CLASS'), | |
| ], | |
| } | |
| replacements = all_replacements.get(style, all_replacements["maxcringe"]) | |
| for pattern, repl in replacements: | |
| text = re.sub(pattern, repl, text, flags=re.IGNORECASE) | |
| return text | |
| def apply_template(text, style): | |
| templates = { | |
| "corporate": { | |
| "prefixes": [ | |
| "Thrilled to announce that ", | |
| "Excited to share that ", | |
| "Pleased to report that ", | |
| "After extensive cross-functional alignment, ", | |
| "Following a comprehensive deep dive, ", | |
| ], | |
| "suffixes": [ | |
| "\n\nThe synergies here are truly remarkable. Looking forward to circling back on next steps. 💼", | |
| "\n\nThis represents a significant value-add for all stakeholders. Let's take this offline to discuss further. 📊", | |
| "\n\nThe ROI on this initiative speaks for itself. Excited to operationalize at scale. 🎯", | |
| "\n\nKey takeaway: when we align our strategic priorities and execute with velocity, the results speak for themselves. 💼", | |
| ], | |
| }, | |
| "humblebrag": { | |
| "prefixes": [ | |
| "Humbled and honored to share that ", | |
| "I don't say this to brag, but ", | |
| "Still processing the fact that ", | |
| "Overwhelmed with gratitude that ", | |
| "Pinch me moment: ", | |
| "Blessed beyond measure to announce that ", | |
| ], | |
| "suffixes": [ | |
| "\n\nI am not worthy of this blessing. Grateful beyond words. 🙏✨", | |
| "\n\nThis wouldn't have been possible without my village. I'm just so humbled. 😌💫", | |
| "\n\nFeeling so much gratitude. If I can achieve this, anyone can. 🙏", | |
| "\n\nHumbled. Honored. Blessed. Grateful. Overwhelmed. 🙏✨💫", | |
| ], | |
| }, | |
| "hustle": { | |
| "prefixes": [ | |
| "While you were sleeping, ", | |
| "While others were resting, ", | |
| "No days off. ", | |
| "The grind doesn't stop. ", | |
| "4:30 AM. Coffee. Grind. Repeat. ", | |
| "While they were making excuses, ", | |
| ], | |
| "suffixes": [ | |
| "\n\n💤 Sleep is for the weak. 💪 Grind never stops. 🔥 No excuses.", | |
| "\n\nThe grindset is real. While they sleep, we grind. While they complain, we build. 💪🔥", | |
| "\n\nRemember: The only thing standing between you and your dreams is the snooze button. ⏰💪", | |
| "\n\n🔥 Success isn't given. It's earned. On the grind. Every. Single. Day. 💪", | |
| ], | |
| }, | |
| "thoughtleader": { | |
| "prefixes": [ | |
| "Hot take: ", | |
| "Unpopular opinion: ", | |
| "Here's what nobody tells you: ", | |
| "Let that sink in: ", | |
| "I've been saying this for years: ", | |
| "A thread 🧵: ", | |
| ], | |
| "suffixes": [ | |
| "\n\nLet that sink in. 🧠\n\nAgree? Drop a 👍\nDisagree? Let's debate.", | |
| "\n\nI said what I said. 🎤 The truth hurts, but it also sets you free. 🧠", | |
| "\n\nMost won't understand this. The ones who do? They're already ahead. 🧠💡", | |
| "\n\nSave this post. Come back to it in 6 months. You'll see. 🧠⏰", | |
| ], | |
| }, | |
| "techbro": { | |
| "prefixes": [ | |
| "🚀 Just shipped: ", | |
| "⚡ Building in public update: ", | |
| "💻 Late night coding session results: ", | |
| "🔥 What I learned while building: ", | |
| "🛠️ MVP update: ", | |
| ], | |
| "suffixes": [ | |
| "\n\n🚀 Shipping > Talking. Building > Planning. Iterating > Perfecting. Let's go! ⚡", | |
| "\n\nShip fast. Break things. Iterate. Scale. Repeat. 🚀⚡ #BuildInPublic", | |
| "\n\nIf you're not embarrassed by your first version, you shipped too late. 🚀 Shipped. Learned. Iterating. ⚡", | |
| ], | |
| }, | |
| "motivational": { | |
| "prefixes": [ | |
| "🔥 REMINDER: ", | |
| "⚡ WAKE UP CALL: ", | |
| "💫 TRUTH BOMB: ", | |
| "🌟 DON'T SCROLL PAST THIS: ", | |
| "✨ READ THIS IF YOU'RE STRUGGLING: ", | |
| "👑 DAILY AFFIRMATION: ", | |
| ], | |
| "suffixes": [ | |
| "\n\n✨ Your only limit is you. Believe it. Achieve it. 🔥\n\nLike if you agree. Share if you care. Comment 'YES' if you're ready.", | |
| "\n\n💪 You are stronger than you think. Braver than you believe. ✨\n\nTag someone who needs to hear this today.", | |
| "\n\n🔥 The comeback is ALWAYS greater than the setback. Keep pushing. Keep going. ✨\n\nWho's with me? Drop a 🔥 below.", | |
| "\n\n✨ Remember: Every expert was once a beginner. Your time is NOW. 💪\n\nShare this with someone who needs it.", | |
| ], | |
| }, | |
| "maxcringe": { | |
| "prefixes": [ | |
| "🚨 STOP SCROLLING! 🚨\n\nHumbled. Honored. Blessed. Thrilled. Overwhelmed. Grateful. Shaking. ", | |
| "I literally can't even right now. ", | |
| "THIS IS NOT A DRILL 🔥🔥🔥\n\nHumbled and honored to announce that ", | |
| "Pinch me. Slap me. Call me crazy. ", | |
| "🚀💥 GAME. CHANGER. 💥🚀\n\n", | |
| ], | |
| "suffixes": [ | |
| "\n\n🙏 Humbled. Honored. Blessed. Grateful. Overwhelmed. Literally shaking.\n\n💥 Agree? Like!\n💬 Disagree? Comment!\n🔄 Care? Share!\n\n#Grateful #Blessed #Humbled #LinkedInLife #Motivation #Hustle #Grind #Success", | |
| "\n\nThis is what they don't teach you in school. 🎓❌\n\n🔥🔥🔥\n\nWho else relates? Comment 'YES' below!\n\n#Leadership #Success #Motivation #Inspiration #Hustle #Grindset #Mindset #Growth #Goals #DreamBig", | |
| "\n\nLet that sink in. 🧠 Now circle back and leverage this. 💼 Then go grind. 💪 And ship it. 🚀\n\nBEYOND HUMBLED. 🙏✨🔥💫\n\n#Synergy #Hustle #Grindset #Leadership #ThoughtLeadership #BuildInPublic #Grateful #Blessed #Mindset #10x #Scale #Disrupt", | |
| "\n\nIf this post inspired even ONE person, it was worth it. 🥺🙏\n\n👉 Like if you're a GO-GETTER\n👉 Comment if you're UNSTOPPABLE\n👉 Share if you DARE TO DREAM\n\n#Motivation #Inspiration #Leadership #Blessed #Humble #Grind #Success #Winning #Champion #Mindset", | |
| ], | |
| }, | |
| } | |
| template = templates.get(style, templates["maxcringe"]) | |
| prefix = random.choice(template["prefixes"]) | |
| suffix = random.choice(template["suffixes"]) | |
| if text and text[0].islower(): | |
| text = text[0].upper() + text[1:] | |
| return f"{prefix}{text}{suffix}" | |
| def add_emojis(text, style): | |
| emoji_sets = { | |
| "corporate": ["📊", "📈", "🎯", "💼", "🏢", "💡", "📋", "🔑", "⚙️"], | |
| "humblebrag": ["🙏", "✨", "💫", "😌", "🌟", "🥺", "💝", "🦋", "💎"], | |
| "hustle": ["💪", "🔥", "⏰", "🏃", "💨", "🏋️", "🦁", "⚡", "🎯"], | |
| "thoughtleader": ["🧠", "💡", "🤔", "👁️", "🎙️", "🔍", "📌", "⚖️"], | |
| "techbro": ["🚀", "⚡", "💻", "🛠️", "🔧", "📱", "🤖", "⌨️", "🖥️"], | |
| "motivational": ["🔥", "✨", "💪", "🌟", "💫", "👑", "🦁", "⭐", "🏆"], | |
| "maxcringe": ["🔥", "🚀", "💪", "🙏", "✨", "💫", "🧠", "💼", "⚡", "📈", "🎯", "💎", "👑", "🌟", "👏"], | |
| } | |
| emojis = emoji_sets.get(style, emoji_sets["maxcringe"]) | |
| sentences = re.split(r'(?<=[.!?])\s+', text) | |
| result = [] | |
| for i, sentence in enumerate(sentences): | |
| result.append(sentence) | |
| if i < len(sentences) - 1 and random.random() > 0.45: | |
| result.append(f" {random.choice(emojis)}") | |
| text = " ".join(result) | |
| return text | |
| def add_hashtags(text, style): | |
| hashtags = { | |
| "corporate": ["#Leadership", "#Strategy", "#Innovation", "#BusinessGrowth", "#Management", "#StakeholderValue"], | |
| "humblebrag": ["#Blessed", "#Grateful", "#Humbled", "#Honored", "#Thankful", "#Overwhelmed"], | |
| "hustle": ["#Hustle", "#Grind", "#NoDaysOff", "#5AMClub", "#Motivation", "#Grindset", "#EntrepreneurLife"], | |
| "thoughtleader": ["#ThoughtLeadership", "#HotTake", "#UnpopularOpinion", "#ThinkDifferent", "#Contrarian", "#Insights"], | |
| "techbro": ["#BuildInPublic", "#StartupLife", "#Shipping", "#TechStartup", "#MVP", "#Disrupt", "#Iteration"], | |
| "motivational": ["#Motivation", "#Inspiration", "#BelieveInYourself", "#NeverGiveUp", "#DreamBig", "#YouGotThis"], | |
| "maxcringe": ["#LinkedInLife", "#Grateful", "#Blessed", "#Humbled", "#Hustle", "#Leadership", "#Motivation", "#Inspiration", "#Growth", "#Mindset", "#Success", "#10x", "#Disrupt", "#Synergy", "#Grind"], | |
| } | |
| tags = hashtags.get(style, hashtags["maxcringe"]) | |
| num_tags = random.randint(3, min(6, len(tags))) | |
| selected = random.sample(tags, num_tags) | |
| return f"{text}\n\n{' '.join(selected)}" | |
| def translate_with_rules(text, style_name): | |
| style_info = STYLES.get(style_name, STYLES["Maximum Cringe 😬"]) | |
| style_key = style_info["key"] | |
| text, was_spun = apply_positivity_spin(text) | |
| result = apply_replacements(text, style_key) | |
| result = apply_template(result, style_key) | |
| result = add_emojis(result, style_key) | |
| result = add_hashtags(result, style_key) | |
| return result, was_spun | |
| # ============================================================ | |
| # TRANSLATION ENGINE — AI-POWERED | |
| # ============================================================ | |
| def translate_with_ai(text, style_name, api_key): | |
| style_info = STYLES.get(style_name, STYLES["Maximum Cringe 😬"]) | |
| style_key = style_info["key"] | |
| style_prompts = { | |
| "corporate": ( | |
| "exaggerated corporate buzzword LinkedIn speak. Use synergy, leverage, paradigm shift, " | |
| "circle back, deep dive, move the needle, operationalize, net-new, mission-critical, " | |
| "value-add, stakeholders, cross-functional. IMPORTANT: Always spin anything negative " | |
| "into a positive opportunity. Never say anything bad directly." | |
| ), | |
| "humblebrag": ( | |
| "humble brag LinkedIn speak. Start with being humbled and honored, mention blessings " | |
| "and gratitude while definitely bragging. Say things like 'I don't say this to brag but...' " | |
| "then brag. IMPORTANT: Always spin anything negative into a blessing in disguise." | |
| ), | |
| "hustle": ( | |
| "hustle culture LinkedIn speak. 5AM grind, no days off, outwork everyone, grindset, " | |
| "beast mode. IMPORTANT: Always spin anything negative as fuel for the grind, a stepping " | |
| "stone, or being tested by the universe." | |
| ), | |
| "thoughtleader": ( | |
| "thought leader LinkedIn speak. Hot takes, unpopular opinions, 'let that sink in', " | |
| "contrarian takes nobody asked for. IMPORTANT: Always spin negatives as profound lessons " | |
| "or hidden truths that most people miss." | |
| ), | |
| "techbro": ( | |
| "tech bro LinkedIn speak. Shipping, building in public, MVP, PMF, disrupting, iterating, " | |
| "scaling. IMPORTANT: Always spin negatives as pivots, learning experiences, or 'iterating " | |
| "on the product'. Never admit failure, only pivoting." | |
| ), | |
| "motivational": ( | |
| "over-the-top motivational speaker LinkedIn speak. ALL CAPS REMINDERS, you are enough, " | |
| "believe in yourself, never give up, inspirational but cringe. IMPORTANT: Always spin " | |
| "negatives as SETBACKS that are really SETUPS for COMEBACKS." | |
| ), | |
| "maxcringe": ( | |
| "maximum cringe LinkedIn speak. Combine ALL LinkedIn stereotypes: corporate buzzwords, " | |
| "humble brags, hustle culture, excessive emojis, hashtag everything, call to actions like " | |
| "'comment YES'. IMPORTANT: ALWAYS spin anything negative into something positive. If someone " | |
| "lost their job, say they 'embraced a new chapter'. If they failed, say they 'discovered a " | |
| "powerful lesson'. NEVER be negative. Everything is a blessing, opportunity, or stepping stone." | |
| ), | |
| } | |
| style_prompt = style_prompts.get(style_key, style_prompts["maxcringe"]) | |
| prompt = f"""<s>[INST] You are the LinkedIn Language Translator. Your #1 rule: NEVER say anything negative directly. Everything must be spun positive. Lost a job = "new chapter". Failed = "discovered a lesson". Fired = "freed to pursue bigger things". | |
| Convert the following normal English into {style_prompt}. Only output the LinkedIn post, nothing else. No explanations. No quotes around the output. | |
| Normal English: {text}[/INST]""" | |
| models = [ | |
| "HuggingFaceH4/zephyr-7b-beta", | |
| "mistralai/Mistral-7B-Instruct-v0.2", | |
| "microsoft/phi-2", | |
| ] | |
| last_error = None | |
| for model in models: | |
| try: | |
| api_url = f"https://api-inference.huggingface.co/models/{model}" | |
| headers = {"Authorization": f"Bearer {api_key}"} | |
| payload = { | |
| "inputs": prompt, | |
| "parameters": { | |
| "max_new_tokens": 400, | |
| "temperature": 0.9, | |
| "top_p": 0.95, | |
| "do_sample": True, | |
| "return_full_text": False, | |
| }, | |
| } | |
| response = requests.post(api_url, headers=headers, json=payload, timeout=60) | |
| if response.status_code == 200: | |
| result = response.json() | |
| if isinstance(result, list) and len(result) > 0: | |
| generated = result[0].get("generated_text", "").strip() | |
| for tag in ["</s>", "<s>", "[/INST]", "[INST]"]: | |
| generated = generated.replace(tag, "") | |
| generated = generated.strip() | |
| if len(generated) > 20: | |
| return generated, None | |
| elif response.status_code == 503: | |
| try: | |
| error_info = response.json() | |
| wait_time = min(error_info.get("estimated_time", 20), 30) | |
| except Exception: | |
| wait_time = 15 | |
| time.sleep(wait_time) | |
| response = requests.post(api_url, headers=headers, json=payload, timeout=60) | |
| if response.status_code == 200: | |
| result = response.json() | |
| if isinstance(result, list) and len(result) > 0: | |
| generated = result[0].get("generated_text", "").strip() | |
| for tag in ["</s>", "<s>", "[/INST]", "[INST]"]: | |
| generated = generated.replace(tag, "") | |
| generated = generated.strip() | |
| if len(generated) > 20: | |
| return generated, None | |
| else: | |
| last_error = f"Model {model}: HTTP {response.status_code}" | |
| elif response.status_code == 401: | |
| return None, "Invalid API token. Check your HuggingFace token and try again." | |
| elif response.status_code == 429: | |
| last_error = f"Rate limited on {model}. Trying next..." | |
| time.sleep(2) | |
| continue | |
| else: | |
| last_error = f"Model {model}: HTTP {response.status_code}" | |
| continue | |
| except requests.exceptions.Timeout: | |
| last_error = f"Model {model} timed out." | |
| continue | |
| except Exception as e: | |
| last_error = f"Error with {model}: {str(e)[:80]}" | |
| continue | |
| return None, last_error or "All AI models unavailable. Using rule-based fallback." | |
| # ============================================================ | |
| # UTILITY FUNCTIONS | |
| # ============================================================ | |
| def calculate_cringe_score(text): | |
| buzzwords = [ | |
| 'synergy', 'leverage', 'paradigm', 'circle back', 'deep dive', | |
| 'bandwidth', 'humbled', 'honored', 'blessed', 'grateful', | |
| 'overwhelmed', 'hustle', 'grind', '5am', 'no days off', | |
| 'hot take', 'let that sink in', 'unpopular opinion', | |
| 'shipping', 'building in public', 'mvp', 'disrupt', | |
| 'reminder', 'game-changer', 'net-new', 'mission-critical', | |
| 'rockstar', 'thought leader', 'grindset', '10x', 'pmf', | |
| 'new chapter', 'opportunity', 'blessing in disguise', | |
| 'redirected', 'season of', 'growth opportunity', | |
| ] | |
| text_lower = text.lower() | |
| score = 0 | |
| for word in buzzwords: | |
| score += text_lower.count(word) * 3 | |
| emoji_pattern = re.compile( | |
| "[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF" "\U0001F680-\U0001F6FF" | |
| "\U0001F1E0-\U0001F1FF" "\U00002702-\U000027B0" "\U0001F900-\U0001F9FF" | |
| "\U00002600-\U000026FF" "\U0000FE00-\U0000FE0F" "\U0001FA00-\U0001FA6F" | |
| "\U0001FA70-\U0001FAFF" "\U000027BF" "]+", flags=re.UNICODE, | |
| ) | |
| score += len(emoji_pattern.findall(text)) * 1 | |
| score += len(re.findall(r'#\w+', text)) * 2 | |
| score += len(re.findall(r'\b[A-Z]{3,}\b', text)) * 2 | |
| ctas = ['like if', 'share if', 'comment below', 'drop a', 'tag someone', | |
| 'agree?', 'who else', 'comment yes', 'read this', 'stop scrolling'] | |
| for cta in ctas: | |
| if cta in text_lower: | |
| score += 4 | |
| return min(100, score) | |
| def get_cringe_level(score): | |
| if score <= 20: | |
| return "Mildly Corporate", "#4CAF50" | |
| elif score <= 40: | |
| return "Buzzword Alert", "#8BC34A" | |
| elif score <= 60: | |
| return "Pretty Cringe", "#FFC107" | |
| elif score <= 80: | |
| return "Maximum Cringe", "#FF5722" | |
| else: | |
| return "Nuclear Cringe ☢️", "#F44336" | |
| # ============================================================ | |
| # MAIN APP | |
| # ============================================================ | |
| def main(): | |
| inject_css(st.session_state.theme) | |
| # ---- SIDEBAR ---- | |
| with st.sidebar: | |
| st.markdown("## ⚙️ Settings") | |
| # Theme toggle | |
| st.markdown("#### 🎨 Theme") | |
| col_t1, col_t2 = st.columns(2) | |
| with col_t1: | |
| if st.button("🌙 Dark", use_container_width=True, | |
| type="primary" if st.session_state.theme == "dark" else "secondary"): | |
| st.session_state.theme = "dark" | |
| st.rerun() | |
| with col_t2: | |
| if st.button("☀️ Light", use_container_width=True, | |
| type="primary" if st.session_state.theme == "light" else "secondary"): | |
| st.session_state.theme = "light" | |
| st.rerun() | |
| st.markdown("<hr class='divider-line'>", unsafe_allow_html=True) | |
| # Style selector | |
| st.markdown("#### 🎭 Translation Style") | |
| style_names = list(STYLES.keys()) | |
| selected_style = st.selectbox( | |
| "Choose your LinkedIn persona:", | |
| style_names, index=0, label_visibility="collapsed", | |
| ) | |
| style_info = STYLES[selected_style] | |
| fg = "#c9d1d9" if st.session_state.theme == "dark" else "#24292f" | |
| st.markdown( | |
| f"<div style='padding:8px 12px;border-radius:8px;background:{style_info['color']}22;" | |
| f"border-left:3px solid {style_info['color']};font-size:0.82rem;color:{fg}'>" | |
| f"{style_info['desc']}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<hr class='divider-line'>", unsafe_allow_html=True) | |
| # AI Mode | |
| st.markdown("#### 🤖 AI Mode") | |
| use_ai = st.toggle("Enable AI Translation", value=False) | |
| if use_ai: | |
| st.caption("Uses HuggingFace Inference API for smarter, more creative translations.") | |
| # Using key="hf_api_key" persists the token in session state automatically | |
| st.text_input( | |
| "HF API Token", type="password", placeholder="hf_xxxxxxxxxxxxx", | |
| help="Get a free token at huggingface.co/settings/tokens", | |
| key="hf_api_key" | |
| ) | |
| # Read from session state so it persists across reruns | |
| hf_api_key = st.session_state.get("hf_api_key", "") | |
| if not hf_api_key: | |
| st.warning("⚠️ Enter your free HF token for AI mode. Falls back to rules if empty.") | |
| else: | |
| hf_api_key = "" | |
| st.markdown("<hr class='divider-line'>", unsafe_allow_html=True) | |
| # Stats | |
| st.markdown("#### 📊 Stats") | |
| col_s1, col_s2 = st.columns(2) | |
| with col_s1: | |
| st.markdown( | |
| f"<div class='stat-box'><div class='stat-value'>{st.session_state.translation_count}</div>" | |
| f"<div class='stat-label'>Translations</div></div>", unsafe_allow_html=True, | |
| ) | |
| with col_s2: | |
| st.markdown( | |
| f"<div class='stat-box'><div class='stat-value'>{len(st.session_state.history)}</div>" | |
| f"<div class='stat-label'>In History</div></div>", unsafe_allow_html=True, | |
| ) | |
| st.markdown("<hr class='divider-line'>", unsafe_allow_html=True) | |
| # History | |
| st.markdown("#### 📜 History") | |
| if st.session_state.history: | |
| if st.button("🗑️ Clear History", use_container_width=True): | |
| st.session_state.history = [] | |
| st.rerun() | |
| for item in reversed(st.session_state.history[-10:]): | |
| truncated_input = item["input"][:50] + ("..." if len(item["input"]) > 50 else "") | |
| style_color = STYLES.get(item["style"], {}).get("color", "#0A66C2") | |
| source_icon = "🤖" if item.get("ai") else "⚙️" | |
| st.markdown( | |
| f"<div class='history-item'>" | |
| f"<span class='style-badge' style='background:{style_color}'>{item['style'].split(' ')[0]}</span> " | |
| f"<span style='font-size:0.75rem'>{source_icon}</span><br>" | |
| f"<b>Input:</b> {html_lib.escape(truncated_input)}<br>" | |
| f"<b>Time:</b> {item['time']} • <b>Cringe:</b> {item['cringe']}/100" | |
| f"</div>", unsafe_allow_html=True, | |
| ) | |
| else: | |
| st.caption("No translations yet. Start translating!") | |
| # ---- MAIN CONTENT ---- | |
| # Header | |
| st.markdown('<h1 class="main-title">LinkedIn Translator</h1>', unsafe_allow_html=True) | |
| st.markdown('<p class="subtitle">Transform normal English into the language of LinkedIn 🤝</p>', unsafe_allow_html=True) | |
| # Example chips | |
| st.markdown("##### 💡 Try an example:") | |
| chip_cols = st.columns(4) | |
| for i, example in enumerate(EXAMPLE_INPUTS[:8]): | |
| col_idx = i % 4 | |
| with chip_cols[col_idx]: | |
| if st.button(example, key=f"ex_{i}", use_container_width=True): | |
| st.session_state.input_area = example | |
| st.rerun() | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # Input / Output columns | |
| col_input, col_output = st.columns(2) | |
| with col_input: | |
| st.markdown( | |
| "<div class='input-card'><div class='card-label input-label'>📝 Normal English</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| input_text = st.text_area( | |
| "Enter your text:", | |
| height=200, | |
| label_visibility="collapsed", | |
| placeholder="Type something normal here...\n\ne.g., I lost my job", | |
| key="input_area", | |
| ) | |
| char_count = len(input_text) if input_text else 0 | |
| word_count = len(input_text.split()) if input_text and input_text.strip() else 0 | |
| st.markdown( | |
| f"<div style='font-size:0.75rem;color:#8b949e;text-align:right;'>" | |
| f"{word_count} words • {char_count}/2000 characters</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with col_output: | |
| st.markdown( | |
| "<div class='output-card'><div class='card-label output-label'>💼 LinkedIn Language</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| if st.session_state.last_output: | |
| safe_output = html_lib.escape(st.session_state.last_output).replace("\n", "<br>") | |
| # Build tags row | |
| tags = [] | |
| if st.session_state.get("last_ai_used"): | |
| tags.append('<span class="ai-tag">🤖 AI Generated</span>') | |
| else: | |
| tags.append('<span class="rule-tag">⚙️ Rule-Based</span>') | |
| if st.session_state.get("was_spun"): | |
| tags.append('<span class="spin-tag">✨ Positivity Spin</span>') | |
| tags_html = f"<div class='tags-container'>{'' .join(tags)}</div>" | |
| st.markdown( | |
| f"{tags_html}<div class='output-display'>{safe_output}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| else: | |
| st.markdown( | |
| "<div class='output-empty'>Your LinkedIn masterpiece will appear here ✨</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Action buttons | |
| col_b1, col_b2, col_b3, col_b4 = st.columns([2, 1, 1, 1]) | |
| with col_b1: | |
| translate_btn = st.button("🔄 **Translate to LinkedIn**", use_container_width=True, type="primary") | |
| with col_b2: | |
| copy_btn = st.button("📋 Copy", use_container_width=True) | |
| with col_b3: | |
| clear_btn = st.button("🗑️ Clear", use_container_width=True) | |
| with col_b4: | |
| download_btn = st.download_button( | |
| "💾 Save", | |
| data=st.session_state.last_output or "", | |
| file_name="linkedin_post.txt", | |
| mime="text/plain", | |
| use_container_width=True, | |
| disabled=not st.session_state.last_output, | |
| ) | |
| # ---- BUTTON ACTIONS ---- | |
| if translate_btn: | |
| if not input_text or not input_text.strip(): | |
| st.toast("⚠️ Please enter some text to translate!", icon="⚠️") | |
| elif len(input_text) > 2000: | |
| st.toast("⚠️ Text too long! Max 2000 characters.", icon="⚠️") | |
| else: | |
| with st.spinner("🔄 Translating to LinkedIn-speak..."): | |
| output_text = "" | |
| was_spun = False | |
| ai_used = False | |
| ai_error = None | |
| if use_ai and hf_api_key: | |
| output_text, ai_error = translate_with_ai(input_text, selected_style, hf_api_key) | |
| if output_text: | |
| ai_used = True | |
| st.toast("✅ AI translation complete!", icon="🤖") | |
| else: | |
| output_text, was_spun = translate_with_rules(input_text, selected_style) | |
| if ai_error: | |
| st.toast(f"⚠️ AI unavailable — {ai_error}", icon="⚡") | |
| else: | |
| st.toast("⚠️ AI unavailable — used rule-based fallback.", icon="⚡") | |
| else: | |
| output_text, was_spun = translate_with_rules(input_text, selected_style) | |
| time.sleep(0.3) | |
| st.toast("✅ Translation complete!", icon="💼") | |
| # Store results | |
| st.session_state.last_output = output_text | |
| st.session_state.last_input = input_text | |
| st.session_state.last_style = selected_style | |
| st.session_state.was_spun = was_spun | |
| st.session_state.last_ai_used = ai_used | |
| st.session_state.translation_count += 1 | |
| cringe = calculate_cringe_score(output_text) | |
| st.session_state.history.append({ | |
| "input": input_text, | |
| "output": output_text, | |
| "style": selected_style, | |
| "cringe": cringe, | |
| "time": datetime.now().strftime("%H:%M"), | |
| "ai": ai_used, | |
| "spun": was_spun, | |
| }) | |
| st.rerun() | |
| if copy_btn: | |
| if st.session_state.last_output: | |
| safe_text = st.session_state.last_output.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$").replace("\n", "\\n") | |
| components.html( | |
| f""" | |
| <script> | |
| (function() {{ | |
| const text = `{safe_text}`.replace(/\\\\n/g, '\\n'); | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = text; | |
| textarea.style.position = 'fixed'; | |
| textarea.style.opacity = '0'; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| try {{ | |
| document.execCommand('copy'); | |
| }} catch(e) {{ | |
| navigator.clipboard.writeText(text).catch(function(){{}}); | |
| }} | |
| document.body.removeChild(textarea); | |
| }})(); | |
| </script> | |
| """, | |
| height=0, | |
| ) | |
| st.toast("📋 Copied to clipboard!", icon="✅") | |
| else: | |
| st.toast("⚠️ Nothing to copy!", icon="⚠️") | |
| if clear_btn: | |
| st.session_state.last_output = "" | |
| st.session_state.last_input = "" | |
| st.session_state.was_spun = False | |
| st.session_state.last_ai_used = False | |
| st.session_state.input_area = "" | |
| st.rerun() | |
| # ---- CRINGE METER ---- | |
| if st.session_state.last_output: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| cringe_score = calculate_cringe_score(st.session_state.last_output) | |
| cringe_level, cringe_color = get_cringe_level(cringe_score) | |
| st.markdown("##### 📊 Cringe Meter") | |
| col_cm1, col_cm2 = st.columns([3, 1]) | |
| with col_cm1: | |
| st.markdown( | |
| f"<div class='cringe-bar-bg'>" | |
| f"<div class='cringe-bar-fill' style='width:{cringe_score}%;background:{cringe_color};'></div>" | |
| f"</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| with col_cm2: | |
| st.markdown( | |
| f"<div style='text-align:center;font-size:1.1rem;font-weight:800;color:{cringe_color};'>" | |
| f"{cringe_score}/100 — {cringe_level}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| # Show spin notice | |
| if st.session_state.get("was_spun"): | |
| st.info("✨ **Positivity Spin Applied** — Negative phrases were automatically rephrased because on LinkedIn, everything is an opportunity! 🌈") | |
| # ---- FOOTER ---- | |
| st.markdown("<br><br>", unsafe_allow_html=True) | |
| footer_color = "#484f58" if st.session_state.theme == "dark" else "#8b949e" | |
| st.markdown( | |
| f"<div style='text-align:center;font-size:0.78rem;color:{footer_color};'>" | |
| f"Built with ❤️ and excessive buzzwords • LinkedIn Translator v2.1<br>" | |
| f"Rule-based translation always works • Add HF API token for AI-powered translations<br>" | |
| f"✨ Positivity Spin: Because on LinkedIn, nothing is ever truly negative" | |
| f"</div>", unsafe_allow_html=True, | |
| ) | |
| if __name__ == "__main__": | |
| main() |