File size: 7,995 Bytes
858836b
33e6a1b
858836b
 
cf081f2
858836b
cf081f2
858836b
cf081f2
e8284c8
858836b
33e6a1b
858836b
 
ac8782d
 
 
858836b
 
cf081f2
858836b
cf081f2
 
858836b
33e6a1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cf081f2
 
 
ac8782d
cf081f2
 
 
 
 
 
 
 
 
 
 
 
 
33e6a1b
 
 
c7b9c40
 
 
 
 
 
 
33e6a1b
 
 
 
 
cf081f2
 
 
 
 
 
ac8782d
cf081f2
 
858836b
cf081f2
 
 
ac8782d
cf081f2
 
 
 
 
 
 
 
 
 
ac8782d
 
cf081f2
 
 
 
 
858836b
ac8782d
 
 
cf081f2
 
858836b
ac8782d
cf081f2
 
 
 
 
 
 
 
 
ac8782d
cf081f2
 
 
858836b
ac8782d
 
33e6a1b
cf081f2
858836b
ac8782d
cf081f2
 
858836b
 
cf081f2
33e6a1b
 
 
 
 
ac8782d
 
 
 
cf081f2
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import os
import re
import streamlit as st
from llm_groq import generate_post, transform_post, generate_hooks, DEFAULT_MODEL
from prompts import build_quick_prompt, build_post_prompt, transform_instruction
from data_utils import load_posts, extract_keywords, dedupe_sentences, strip_labels
from ui_components import quick_controls, pro_controls

st.set_page_config(page_title="LinkedIn Post Generator — Groq", layout="centered")
st.title("LinkedIn Post Generator — Quick & Pro ")

# Sidebar
with st.sidebar:
    st.subheader("Groq & Decoding")
    model = st.selectbox("Model", [DEFAULT_MODEL, "llama-3.1-8b-instant", "mixtral-8x7b-32768"], index=0, key="sb_model")
    temperature = st.slider("Temperature", 0.1, 1.2, 0.6, 0.05, key="sb_temp")
    top_p = st.slider("Top‑p", 0.1, 1.0, 0.9, 0.05, key="sb_topp")
    st.markdown("Set GROQ_API_KEY in Space → Settings → Variables & Secrets.")

tabs = st.tabs(["Quick Draft", "Pro Mode", "History"])

if "history" not in st.session_state:
    st.session_state.history = []

def quick_quality_fix(text, want_hashtags=True, allow_emoji=True):
    lines = [l for l in text.strip().splitlines() if l.strip()]
    if len(lines) < 4 or len(lines) > 7:
        return None
    if not allow_emoji:
        text = re.sub(r"[^\w\s#.,:;%&()\-\+\[\]{}'\"/]", "", text)
    tags = re.findall(r"#\w+", text)
    if not want_hashtags and tags:
        for t in tags:
            text = text.replace(t, "")
    if want_hashtags and len(tags) > 2:
        for t in tags[2:]:
            text = text.replace(t, "")
    return text.strip()

# Quick Draft
with tabs[0]:
    idea, tone, words, variations, include_emoji, add_hashtags, language = quick_controls()
    if st.button("Generate", key="qd_generate"):
        if not os.getenv("GROQ_API_KEY"):
            st.error("GROQ_API_KEY missing.")
        elif not idea.strip():
            st.warning("Enter your idea.")
        else:
            prompt = build_quick_prompt(idea, tone, words, include_emoji, add_hashtags, language)
            posts = []
            with st.spinner("Generating…"):
                try:
                    max_tokens = max(200, min(1200, int(words*1.6)+120))
                    for _ in range(variations):
                        raw = generate_post(prompt, model, temperature, top_p, max_tokens)
                        clean = dedupe_sentences(strip_labels(raw))
                        fixed = quick_quality_fix(clean, want_hashtags=add_hashtags, allow_emoji=include_emoji)
                        if fixed is None:
                            corrective = (
                                prompt
                                +  f"\n\nRegenerate a full LinkedIn post around {words} words total, "
                                   "structured in 4–6 short paragraphs (each 2–3 lines). "
                                   "Keep it scannable, professional, and engaging. "
                                   "include one concrete metric or date, "
                                   f"{'max 5 emoji' if include_emoji else 'no emojis'}, "
                                   f"{'1–2 niche hashtags at the end' if add_hashtags else 'no hashtags'}."
                            )
                            raw2 = generate_post(corrective, model, temperature, top_p, max_tokens)
                            clean2 = dedupe_sentences(strip_labels(raw2))
                            fixed = quick_quality_fix(clean2, want_hashtags=add_hashtags, allow_emoji=include_emoji) or clean2
                        posts.append(fixed)
                except Exception as e:
                    st.error(f"Generation failed: {e}")
                    posts = []
            for i, p in enumerate(posts, start=1):
                st.markdown(f"#### Post {i}")
                st.write(p)
                st.download_button(f"Download Post {i}", p, file_name=f"post_{i}.txt", key=f"qd_dl_{i}")
            if posts:
                st.session_state.history.append({"mode":"quick","idea":idea,"tone":tone,"words":words,"posts":posts})

# Pro Mode
with tabs[1]:
    st.markdown("Upload CSV/JSON of past posts (must include 'text') to auto-extract keywords (optional).")
    uploaded = st.file_uploader("Upload dataset", type=["csv","json"], key="pro_upload")
    defaults = {"topic":"AI agent playbooks for startup ops","audience":"SaaS founders in early stage"}
    topic, purpose, audience, tone2, language2, evidence, style_text = pro_controls(defaults)
    keywords = []
    if uploaded is not None:
        try:
            df = load_posts(uploaded)
            keywords = extract_keywords(topic, df)
            st.success(f"Loaded {len(df)} posts. Extracted keywords: {', '.join(keywords[:8]) or '—'}")
        except Exception as e:
            st.error(f"Dataset error: {e}")

    if st.button("Suggest 5 hooks", key="pro_hooks_btn"):
        try:
            hooks = generate_hooks(topic, audience, tone2, 5, model, temperature, top_p, 200)
            st.code(hooks)
        except Exception as e:
            st.error(f"Hook generation failed: {e}")

    chosen_hook = st.text_input("Chosen opening line (optional)", key="pro_chosen_hook")
    outcome = st.text_input("Desired outcome (e.g., 10 demo requests this week)", value="", key="pro_outcome")
    extra_detail = st.text_input("One concrete detail (e.g., 'onboarding 14→3 days')", value="", key="pro_detail")
    clarifier_notes = "\n".join([f"Outcome: {outcome}" if outcome else "", f"Detail: {extra_detail}" if extra_detail else ""]).strip()
    style_cues = [s.strip() for s in style_text.splitlines() if s.strip()][:4]

    if st.button("Generate Post (Pro)", key="pro_generate"):
        if not os.getenv("GROQ_API_KEY"):
            st.error("GROQ_API_KEY missing.")
        else:
            try:
                prompt = build_post_prompt(topic, language2, tone2, 160, purpose, audience, evidence, keywords, style_cues, clarifier_notes, chosen_hook)
                raw = generate_post(prompt, model, temperature, top_p, 800)
                post = dedupe_sentences(strip_labels(raw))
                st.success("Post")
                st.write(post)
                st.download_button("Download (.txt)", post, file_name="linkedin_post.txt", key="pro_download")
                st.session_state.history.append({"mode":"pro","topic":topic,"audience":audience,"post":post})
            except Exception as e:
                st.error(f"Generation failed: {e}")

    # Refinements
    col1,col2,col3,col4,col5 = st.columns(5)
    def refine(kind):
        if st.session_state.get("history") and st.session_state.history[-1].get("post"):
            try:
                instr = transform_instruction(kind)
                raw = transform_post(instr, st.session_state.history[-1]["post"], model, temperature, top_p, 500)
                st.session_state.history[-1]["post"] = dedupe_sentences(strip_labels(raw))
            except Exception as e:
                st.error(f"Refinement failed: {e}")

    if col1.button("Shorter", key="pro_shorter"): refine("shorter")
    if col2.button("Punchier hook", key="pro_punchy"): refine("punchier")
    if col3.button("Add data point", key="pro_adddata"): refine("add_data")
    if col4.button("No emojis", key="pro_noemoji"): refine("less_emoji")
    if col5.button("Add hashtags", key="pro_addtags"): refine("add_tags")

    if st.session_state.get("history") and st.session_state.history[-1].get("post"):
        st.write(st.session_state.history[-1]["post"])

# History
with tabs[2]:
    if not st.session_state.history:
        st.info("No saved drafts yet.")
    else:
        for i, item in enumerate(reversed(st.session_state.history), start=1):
            st.markdown(f"#### Draft {i} ({item.get('mode')})")
            if "posts" in item:
                for j, p in enumerate(item["posts"], start=1):
                    st.markdown(f"Post {j}")
                    st.write(p)
            else:
                st.write(item.get("post",""))