File size: 15,200 Bytes
94da7e5
 
 
 
ce83e73
 
94da7e5
 
 
ce83e73
 
 
 
94da7e5
 
 
 
 
 
 
 
 
 
 
 
ce83e73
 
 
 
 
94da7e5
 
ce83e73
94da7e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce83e73
94da7e5
 
 
 
ce83e73
 
 
 
 
94da7e5
 
 
 
 
ce83e73
94da7e5
 
 
 
ce83e73
 
 
 
 
 
 
 
 
 
94da7e5
 
 
 
 
 
ce83e73
 
 
 
 
 
94da7e5
 
ce83e73
94da7e5
 
 
ce83e73
 
 
94da7e5
ce83e73
 
 
 
94da7e5
ce83e73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94da7e5
 
 
ce83e73
 
94da7e5
 
 
 
 
 
 
ce83e73
94da7e5
 
 
 
 
 
 
 
 
 
 
ce83e73
94da7e5
 
 
ce83e73
94da7e5
 
 
 
 
 
 
 
 
 
 
 
 
 
ce83e73
94da7e5
ce83e73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94da7e5
ce83e73
94da7e5
 
ce83e73
94da7e5
 
ce83e73
94da7e5
 
ce83e73
94da7e5
 
 
 
 
 
 
ce83e73
94da7e5
 
 
ce83e73
94da7e5
 
 
ce83e73
 
 
 
 
 
94da7e5
 
 
 
 
 
 
 
 
 
 
 
 
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import gradio as gr
import google.generativeai as genai
import json
import os
import requests
from datetime import datetime

# Configure Gemini
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
AIRTABLE_API_KEY = os.environ.get("AIRTABLE_API_KEY", "")
AIRTABLE_BASE_ID = os.environ.get("AIRTABLE_BASE_ID", "app7WFijpDJ46Cib3")
AIRTABLE_TABLE = "Pipeline Queue"

if GEMINI_API_KEY:
    genai.configure(api_key=GEMINI_API_KEY)

NICHES = [
    "passive income", "dropshipping Australia", "lawn care business",
    "pet care business", "AI tools for small business", "freelancing",
    "print on demand", "affiliate marketing", "ecommerce Australia",
    "side hustle ideas", "digital products", "self publishing",
    "social media marketing", "home based business", "trading for beginners",
    "lawn mowing business Australia", "WarmPaws pet products"
]

# ── Global state to hold last results ──────────────────────────────────────────
last_ideas = []

def research_ebook_ideas(niche: str, count: int, api_key: str):
    global last_ideas
    key = api_key.strip() or GEMINI_API_KEY
    if not key:
        return "❌ Please enter a Gemini API key.", "{}", "[]", gr.update(visible=False)

    try:
        genai.configure(api_key=key)
        model = genai.GenerativeModel(
            model_name="gemini-2.0-flash",
            system_instruction=(
                "You are an expert SEO strategist and digital publishing consultant "
                "for Brett Apps, an Australian self-publishing business. "
                "Return ONLY valid JSON β€” no markdown, no backticks, no explanation."
            )
        )

        # Step 1: SEO Keyword Research
        kw_prompt = f"""Generate SEO keyword research for niche: "{niche}" targeting Australian audiences.
Return ONLY valid JSON:
{{
  "niche": "{niche}",
  "primary_keywords": ["kw1","kw2","kw3","kw4","kw5"],
  "long_tail_keywords": ["lt1","lt2","lt3","lt4","lt5"],
  "buyer_intent_keywords": ["bi1","bi2","bi3"],
  "australian_keywords": ["au1","au2","au3"],
  "trending_topics": ["t1","t2","t3"],
  "search_volume_estimate": "high",
  "competition_level": "medium",
  "monetisation_potential": "high"
}}"""

        kw_response = model.generate_content(kw_prompt)
        kw_data = json.loads(kw_response.text.strip())

        # Step 2: Generate eBook Ideas
        all_keywords = (
            kw_data.get("primary_keywords", []) +
            kw_data.get("long_tail_keywords", []) +
            kw_data.get("australian_keywords", [])
        )

        ideas_prompt = f"""Create {count} high-converting eBook ideas for niche "{niche}".
Use these SEO keywords naturally in titles/subtitles: {", ".join(all_keywords[:10])}
Trending topics to reference: {", ".join(kw_data.get("trending_topics", []))}

Rules:
- Titles must include 1-2 primary keywords
- Subtitles should include long-tail or buyer intent keywords
- Target Australian audiences
- Price range $7-$27 AUD
- Word count 8,000-15,000 words

Return ONLY valid JSON:
{{
  "ebook_ideas": [
    {{
      "rank": 1,
      "title": "SEO-optimised title with keyword",
      "subtitle": "Long-tail keyword subtitle for Australian audience",
      "primary_keyword": "main keyword",
      "secondary_keywords": ["k1","k2","k3"],
      "target_audience": "specific audience",
      "pain_point": "core problem solved",
      "unique_angle": "differentiator from competitors",
      "chapter_count": 8,
      "estimated_words": 10000,
      "recommended_price_aud": 17,
      "competition_score": "low",
      "demand_score": "high",
      "seo_score": "high",
      "estimated_monthly_searches": "500-1000",
      "pipeline_ready_topic": "exact topic for pipeline"
    }}
  ]
}}"""

        ideas_response = model.generate_content(ideas_prompt)
        ideas_data = json.loads(ideas_response.text.strip())
        ideas = ideas_data.get("ebook_ideas", [])
        last_ideas = ideas

        # Format output
        output_lines = []
        output_lines.append(f"## πŸ“š eBook Market Research β€” {niche}\n")
        output_lines.append(
            f"**Search Volume:** {kw_data.get('search_volume_estimate','?').upper()} | "
            f"**Competition:** {kw_data.get('competition_level','?').upper()} | "
            f"**Monetisation:** {kw_data.get('monetisation_potential','?').upper()}\n"
        )
        output_lines.append(f"**πŸ”‘ Top Keywords:** {' β€’ '.join(kw_data.get('primary_keywords',[])[:5])}\n")
        output_lines.append(f"**πŸ“ˆ Trending:** {' β€’ '.join(kw_data.get('trending_topics',[]))}\n")
        output_lines.append("---\n")

        for idea in ideas:
            se = {"high": "🟒", "medium": "🟑", "low": "πŸ”΄"}
            output_lines.append(f"### #{idea.get('rank','?')} {idea.get('title','')}")
            output_lines.append(f"*{idea.get('subtitle','')}*\n")
            output_lines.append(f"**πŸ”‘ Primary Keyword:** `{idea.get('primary_keyword','')}`")
            output_lines.append(f"**πŸ” Monthly Searches:** ~{idea.get('estimated_monthly_searches','?')}")
            output_lines.append(
                f"**πŸ’° Price:** ${idea.get('recommended_price_aud','?')} AUD | "
                f"**πŸ“„ Words:** ~{idea.get('estimated_words',0):,} | "
                f"**Chapters:** {idea.get('chapter_count','?')}"
            )
            output_lines.append(
                f"**Demand:** {se.get(idea.get('demand_score',''),'❓')} {idea.get('demand_score','?').upper()} | "
                f"**SEO:** {se.get(idea.get('seo_score',''),'❓')} {idea.get('seo_score','?').upper()} | "
                f"**Competition:** {se.get(idea.get('competition_score',''),'❓')} {idea.get('competition_score','?').upper()}"
            )
            output_lines.append(f"**🎯 Unique Angle:** {idea.get('unique_angle','')}")
            output_lines.append(f"**πŸ‘₯ Audience:** {idea.get('target_audience','')}")
            output_lines.append(f"**😩 Pain Point:** {idea.get('pain_point','')}")
            output_lines.append(f"**πŸš€ Pipeline Topic:** `{idea.get('pipeline_ready_topic','')}`")
            output_lines.append("\n---\n")

        return (
            "\n".join(output_lines),
            json.dumps(kw_data, indent=2),
            json.dumps(ideas, indent=2),
            gr.update(visible=True)
        )

    except json.JSONDecodeError as e:
        return f"❌ JSON parse error: {e}\n\nTry again β€” Gemini occasionally returns malformed JSON.", "{}", "[]", gr.update(visible=False)
    except Exception as e:
        msg = str(e)
        if "429" in msg:
            return "⚠️ Gemini quota exceeded. Wait a minute and try again, or use a different API key.", "{}", "[]", gr.update(visible=False)
        return f"❌ Error: {msg}", "{}", "[]", gr.update(visible=False)


def send_to_pipeline(selected_rank: int, at_key: str):
    """Send the selected eBook idea to the Airtable Pipeline Queue."""
    global last_ideas
    key = at_key.strip() or AIRTABLE_API_KEY

    if not key:
        return "❌ Please enter your Airtable API key."
    if not last_ideas:
        return "❌ No ideas found. Generate ideas first."

    # Find the selected idea (rank is 1-based)
    idx = max(0, selected_rank - 1)
    if idx >= len(last_ideas):
        idx = 0
    idea = last_ideas[idx]

    topic = idea.get("pipeline_ready_topic") or f"{idea.get('title','')} β€” {idea.get('subtitle','')}"

    payload = {
        "fields": {
            "Topic": topic,
            "Title": idea.get("title", ""),
            "Subtitle": idea.get("subtitle", ""),
            "Primary Keyword": idea.get("primary_keyword", ""),
            "Target Audience": idea.get("target_audience", ""),
            "Pain Point": idea.get("pain_point", ""),
            "Unique Angle": idea.get("unique_angle", ""),
            "Recommended Price (AUD)": idea.get("recommended_price_aud", 17),
            "Estimated Words": idea.get("estimated_words", 10000),
            "Chapter Count": idea.get("chapter_count", 8),
            "Demand Score": idea.get("demand_score", ""),
            "SEO Score": idea.get("seo_score", ""),
            "Competition Score": idea.get("competition_score", ""),
            "Monthly Searches": idea.get("estimated_monthly_searches", ""),
            "Status": "Queued",
            "Queued At": datetime.utcnow().isoformat() + "Z",
            "Source": "EbookAgent HF Space",
        }
    }

    try:
        url = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}/{requests.utils.quote(AIRTABLE_TABLE)}"
        response = requests.post(
            url,
            headers={
                "Authorization": f"Bearer {key}",
                "Content-Type": "application/json",
            },
            json=payload,
            timeout=15,
        )

        if response.status_code in (200, 201):
            data = response.json()
            record_id = data.get("id", "unknown")
            return (
                f"βœ… **Sent to Pipeline Queue!**\n\n"
                f"**Topic:** {topic}\n"
                f"**Airtable Record:** `{record_id}`\n"
                f"**Status:** Queued\n\n"
                f"Your eBook pipeline will pick this up on the next run. πŸš€"
            )
        else:
            err = response.json()
            # If table doesn't exist, provide setup instructions
            if "NOT_FOUND" in str(err) or response.status_code == 404:
                return (
                    f"⚠️ **Pipeline Queue table not found in Airtable.**\n\n"
                    f"Create a table called **'Pipeline Queue'** in base `{AIRTABLE_BASE_ID}` with these fields:\n"
                    f"- Topic (Single line text)\n"
                    f"- Title, Subtitle, Primary Keyword, Target Audience (Single line text)\n"
                    f"- Pain Point, Unique Angle (Long text)\n"
                    f"- Recommended Price (AUD), Estimated Words, Chapter Count (Number)\n"
                    f"- Demand Score, SEO Score, Competition Score, Monthly Searches (Single line text)\n"
                    f"- Status (Single select: Queued, In Progress, Done)\n"
                    f"- Queued At (Date), Source (Single line text)\n\n"
                    f"Then click **Send to Pipeline** again."
                )
            return f"❌ Airtable error ({response.status_code}): {json.dumps(err, indent=2)}"

    except Exception as e:
        return f"❌ Request failed: {str(e)}"


# ── Gradio UI ──────────────────────────────────────────────────────────────────
with gr.Blocks(
    title="Brett Apps β€” eBook Market Research Agent",
    theme=gr.themes.Base(
        primary_hue="purple",
        secondary_hue="indigo",
        neutral_hue="slate",
        font=gr.themes.GoogleFont("Inter"),
    ),
    css="""
        .header { text-align: center; padding: 2rem 0 1rem; }
        .header h1 { font-size: 2rem; font-weight: 800; color: #a855f7; }
        .header p { color: #94a3b8; font-size: 0.95rem; }
        .send-box { border: 1px solid #7c3aed; border-radius: 12px; padding: 1rem; background: #1e1b2e; }
        footer { display: none !important; }
    """
) as demo:

    with gr.Column(elem_classes="header"):
        gr.HTML("""
            <h1>πŸ“š eBook Market Research Agent</h1>
            <p>Brett Apps Β· SEO Keyword-Injected eBook Idea Generator Β· brett@brettapps.com</p>
        """)

    with gr.Row():
        # ── Left panel ─────────────────────────────────────────────────────────
        with gr.Column(scale=1):
            api_key_input = gr.Textbox(
                label="πŸ”‘ Gemini API Key",
                placeholder="AIza... (or set GEMINI_API_KEY secret)",
                type="password",
                info="Get your key at aistudio.google.com"
            )
            niche_dropdown = gr.Dropdown(
                choices=NICHES,
                value="passive income",
                label="πŸ“‚ Select Niche",
                allow_custom_value=True,
                info="Choose a niche or type your own"
            )
            count_slider = gr.Slider(
                minimum=1, maximum=10, value=5, step=1,
                label="πŸ’‘ Number of eBook Ideas"
            )
            run_btn = gr.Button("πŸ” Generate eBook Ideas", variant="primary", size="lg")

            gr.Markdown("---")

            # ── Send to Pipeline panel ─────────────────────────────────────────
            with gr.Column(visible=False, elem_classes="send-box") as pipeline_panel:
                gr.Markdown("### πŸš€ Send to Pipeline")
                at_key_input = gr.Textbox(
                    label="Airtable API Key",
                    placeholder="patXXX... (or set AIRTABLE_API_KEY secret)",
                    type="password"
                )
                idea_rank = gr.Slider(
                    minimum=1, maximum=10, value=1, step=1,
                    label="Which idea to send? (by rank #)"
                )
                send_btn = gr.Button("πŸ“€ Send to Airtable Pipeline Queue", variant="secondary")
                pipeline_status = gr.Markdown("")

            gr.Markdown("""
            ---
            **Brett Apps eBook Pipeline**
            `Market Research β†’ Outline β†’ Writer β†’ Design β†’ Publish β†’ Sales`
            """)

        # ── Right panel ────────────────────────────────────────────────────────
        with gr.Column(scale=2):
            output_md = gr.Markdown(
                value="*Results will appear here after generation...*"
            )

    with gr.Accordion("πŸ“¦ Raw JSON Output", open=False):
        with gr.Row():
            kw_json = gr.Code(label="Keyword Data", language="json", lines=15)
            ideas_json = gr.Code(label="eBook Ideas", language="json", lines=15)

    # ── Event handlers ─────────────────────────────────────────────────────────
    run_btn.click(
        fn=research_ebook_ideas,
        inputs=[niche_dropdown, count_slider, api_key_input],
        outputs=[output_md, kw_json, ideas_json, pipeline_panel],
        show_progress=True
    )

    send_btn.click(
        fn=send_to_pipeline,
        inputs=[idea_rank, at_key_input],
        outputs=[pipeline_status]
    )

    gr.Examples(
        examples=[
            ["passive income", 5, ""],
            ["lawn care business", 3, ""],
            ["dropshipping Australia", 5, ""],
            ["AI tools for small business", 5, ""],
        ],
        inputs=[niche_dropdown, count_slider, api_key_input],
        label="Quick Examples"
    )

if __name__ == "__main__":
    demo.launch()