import os, time, sqlite3, json, requests from functools import wraps from flask import Flask, request, jsonify, make_response, render_template_string app = Flask(__name__) app.jinja_env.autoescape = False # --- Config --- PORT = int(os.environ.get("PORT", 5056)) DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "groots.db") ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "43a6c7e3d7b240daafae006e8488f674.ivYYRRLySgUwgqVE") ZAI_API_URL = "https://api.z.ai/api/coding/paas/v4/chat/completions" ZAI_MODEL = "glm-5-turbo" OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "") OPENROUTER_MODEL = "google/gemini-2.0-flash-001" OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" # --- Rate Limiting --- _rl = {} def rate_limit(f): @wraps(f) def wrapped(*args, **kwargs): now = time.time() ip = request.remote_addr if ip not in _rl: _rl[ip] = [] _rl[ip] = [t for t in _rl[ip] if now - t < 60] if len(_rl[ip]) >= 10: return jsonify({"error": "Rate limit bereikt (10/min)"}), 429 _rl[ip].append(now) return f(*args, **kwargs) return wrapped # --- DB --- def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.executescript(""" CREATE TABLE IF NOT EXISTS campaigns (id INTEGER PRIMARY KEY AUTOINCREMENT, theme TEXT, keywords TEXT DEFAULT '', casus TEXT DEFAULT '', doelgroep TEXT DEFAULT '', platforms TEXT DEFAULT '', content_json TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS project_plans (id INTEGER PRIMARY KEY AUTOINCREMENT, theme TEXT, plan_type TEXT, duration_weeks INTEGER DEFAULT 4, content TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS lead_magnets (id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, topic TEXT, content TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS outreach (id INTEGER PRIMARY KEY AUTOINCREMENT, target_group TEXT, messages_json TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP); """) return conn # --- AI --- def call_ai(system_prompt, user_message, max_tokens=2000): messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}] return call_ai_messages(messages, max_tokens) def call_ai_messages(messages, max_tokens=2000): # ZAI primary try: r = requests.post(ZAI_API_URL, headers={"Authorization": f"Bearer {ZAI_API_KEY}", "Content-Type": "application/json"}, json={"model": ZAI_MODEL, "messages": messages, "max_tokens": max_tokens}, timeout=90) if r.status_code == 200: content = r.json()["choices"][0]["message"].get("content", "") if content and len(content.strip()) > 10: return content else: print(f"ZAI returned empty content, trying fallback") else: print(f"ZAI error {r.status_code}: {r.text[:200]}") except Exception as e: print(f"ZAI exception: {e}") # OpenRouter fallback if OPENROUTER_API_KEY: try: r = requests.post(OPENROUTER_URL, headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json"}, json={"model": OPENROUTER_MODEL, "messages": messages, "max_tokens": max_tokens}, timeout=90) if r.status_code == 200: return r.json()["choices"][0]["message"]["content"] else: print(f"OpenRouter error {r.status_code}: {r.text[:200]}") except Exception as e: print(f"OpenRouter exception: {e}") return None # --- Helpers --- def nocache(resp): r = make_response(resp) r.headers["Cache-Control"] = "no-store, no-cache, must-revalidate" r.headers["Pragma"] = "no-cache" return r # --- HTML Templates --- STYLE = """ """ SIDEBAR = """ """ TOPBAR = """
{{topbar_title}}
""" def page(title, subtitle, body, page_id): html = f""" {title} — Groots in Vertellen {STYLE} {render_template_string(SIDEBAR, page=page_id)} {render_template_string(TOPBAR, topbar_title=title)}

{title}

{subtitle}
{body}
""" return nocache(html) # --- Routes --- @app.route("/groots/") def dashboard(): db=get_db() c=db.execute("SELECT COUNT(*) FROM campaigns").fetchone()[0] l=db.execute("SELECT COUNT(*) FROM lead_magnets").fetchone()[0] o=db.execute("SELECT COUNT(*) FROM outreach").fetchone()[0] p=db.execute("SELECT COUNT(*) FROM project_plans").fetchone()[0] db.close() body=f"""
{c}
Campagnes
{l}
Lead Magnets
{o}
Outreach
{p}
Projectplannen

👋 Welkom Ellen

Jouw Social Campaign Engine — genereer content, projectplannen en lead magnets in één klik.

📱 Nieuwe Campagne 📋 Projectplan 🧲 Lead Magnet
""" return page("Dashboard","Overzicht van jouw campagne-activiteiten",body,"dashboard") @app.route("/groots/campaign") def campaign(): body="""
""" return page("Campaign Builder","Genereer LinkedIn posts, Instagram carrousels en video hooks",body,"campaign") @app.route("/groots/project") def project(): body="""
📋 Plan van Aanpak
📄 PVA
📅 Rollout Schema
""" return page("Projectaanpak","Plan van Aanpak, PVA en Rollout Schema generator",body,"project") @app.route("/groots/leads") def leads(): topics=["DISC profielen","Presentatievaardigheden","Communicatie in teams","Train de trainer","Feedback geven","Conflicthantering","Vergadertechnieken","Overtuigend presenteren"] opts="".join(f'' for t in topics) body=f"""
🧲 Lead Magnet Creator
📬 Outreach Assistent
""" return page("Leads & Outreach","Lead Magnets en Outreach berichten generator",body,"leads") @app.route("/groots/projects") def projects(): db=get_db() camps=db.execute("SELECT id,theme,created_at FROM campaigns ORDER BY id DESC LIMIT 50").fetchall() plans=db.execute("SELECT id,theme,plan_type,created_at FROM project_plans ORDER BY id DESC LIMIT 50").fetchall() leads=db.execute("SELECT id,type,topic,created_at FROM lead_magnets ORDER BY id DESC LIMIT 50").fetchall() outreaches=db.execute("SELECT id,target_group,created_at FROM outreach ORDER BY id DESC LIMIT 50").fetchall() db.close() items="" for c in camps: cid=c["id"];ct=c["theme"] or "Geen thema";cd=c["created_at"] items+='
Campagne '+str(ct)+'
'+str(cd)+'
' for p in plans: pid=p["id"];pt=p["theme"] or "Geen thema";pp=p["plan_type"];pd=p["created_at"] items+='
'+str(pp)+' '+str(pt)+'
'+str(pd)+'
' for l in leads: lid=l["id"];lt=l["type"];ltp=l["topic"];ld=l["created_at"] items+='
'+str(lt)+' '+str(ltp)+'
'+str(ld)+'
' for o in outreaches: oid=o["id"];ot=o["target_group"];od=o["created_at"] items+='
Outreach '+str(ot)+'
'+str(od)+'
' if not items: items='
Nog geen projecten. Start met de Campaign Builder!
' body=f'
{items}
' return page("Projecten","Alle opgeslagen projecten en gegenereerde content",body,"projects") # --- Project View & Export API --- @app.route("/groots/api/project//") def api_project_view(table, item_id): tables = {"campaign": "campaigns", "plan": "project_plans", "lead": "lead_magnets", "outreach": "outreach"} if table not in tables: return jsonify({"error": "Ongeldig type"}), 400 db = get_db() row = db.execute(f"SELECT * FROM {tables[table]} WHERE id=?", (item_id,)).fetchone() db.close() if not row: return jsonify({"error": "Niet gevonden"}), 404 type_labels = {"campaign": "Campagne", "plan": "Projectplan", "lead": "Lead Magnet", "outreach": "Outreach"} title = row["theme"] or row["topic"] or row["target_group"] or "Onbekend" content_raw = row["content_json"] if table == "campaign" else row["content"] if content_raw and content_raw.startswith("{"): try: content_raw = json.loads(content_raw) except: pass if isinstance(content_raw, dict): content_raw = json.dumps(content_raw, indent=2, ensure_ascii=False) content_html = str(content_raw or "Geen content beschikbaar").replace("\n", "
") return jsonify({"title": title, "type": type_labels.get(table, table), "date": row["created_at"], "content": content_html}) @app.route("/groots/api/project/
//docx") def api_project_docx(table, item_id): tables = {"campaign": "campaigns", "plan": "project_plans", "lead": "lead_magnets", "outreach": "outreach"} if table not in tables: return jsonify({"error": "Ongeldig type"}), 400 try: from docx import Document from docx.shared import Pt, Cm, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH except ImportError: return jsonify({"error": "python-docx niet beschikbaar"}), 500 db = get_db() row = db.execute(f"SELECT * FROM {tables[table]} WHERE id=?", (item_id,)).fetchone() db.close() if not row: return jsonify({"error": "Niet gevonden"}), 404 type_labels = {"campaign": "Campagne", "plan": "Projectplan", "lead": "Lead Magnet", "outreach": "Outreach"} title = row["theme"] or row["topic"] or row["target_group"] or "Onbekend" content_raw = row["content_json"] if table == "campaign" else row["content"] if content_raw and content_raw.startswith("{"): try: content_raw = json.loads(content_raw) except: pass if isinstance(content_raw, dict): content_raw = json.dumps(content_raw, indent=2, ensure_ascii=False) content_text = str(content_raw or "") doc = Document() style = doc.styles['Normal'] style.font.name = 'Calibri' style.font.size = Pt(11) style.font.color.rgb = RGBColor(0x1F, 0x29, 0x37) h = doc.add_heading(title, level=0) for run in h.runs: run.font.color.rgb = RGBColor(0x7C, 0x3A, 0xED) meta = doc.add_paragraph() run = meta.add_run(f"Type: {type_labels.get(table, table)} | Datum: {row['created_at']} | Groots in Vertellen — Ellen Groot") run.font.size = Pt(9) run.font.color.rgb = RGBColor(0x6B, 0x72, 0x80) footer = doc.sections[0].footer fp = footer.paragraphs[0] fp.alignment = WD_ALIGN_PARAGRAPH.CENTER fr = fp.add_run("Groots in Vertellen | Ellen Groot | grootsinvertellen.nl | © 2026") fr.font.size = Pt(8) fr.font.color.rgb = RGBColor(0x6B, 0x72, 0x80) for line in content_text.split("\n"): line = line.strip() if not line: continue if line.startswith("### "): doc.add_heading(line[4:], level=2) elif line.startswith("## "): doc.add_heading(line[3:], level=1) elif line.startswith("# "): doc.add_heading(line[2:], level=0) elif line.startswith("**") and line.endswith("**"): p = doc.add_paragraph() p.add_run(line.strip("*")).bold = True elif line.startswith("- ") or line.startswith("* "): doc.add_paragraph(line[2:], style='List Bullet') else: doc.add_paragraph(line) import tempfile tmp = tempfile.NamedTemporaryFile(suffix='.docx', delete=False) doc.save(tmp.name) tmp.close() from flask import send_file filename = f"{title}_Groots_{type_labels.get(table, table)}.docx".replace(" ", "_") return send_file(tmp.name, as_attachment=True, download_name=filename, mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document') # --- Delete API --- @app.route("/groots/api/delete", methods=["POST"]) def api_delete(): data = request.get_json(force=True) table = data.get("table", "") item_id = data.get("id", 0) tables = {"campaign": "campaigns", "plan": "project_plans", "lead": "lead_magnets", "outreach": "outreach"} if table in tables: db = get_db() db.execute(f"DELETE FROM {tables[table]} WHERE id=?", (item_id,)) db.commit(); db.close() return jsonify({"ok": True}) return jsonify({"error": "Ongeldig type"}), 400 # --- API --- PROMPTS = { "campaign": "Je bent de content strategist van Ellen Groot — trainer, coach en expert in communicatie, presentatie en DISC.\nSchrijf altijd in de ik-vorm VANUIT Ellen. Toon: authentiek, inspirerend, didactisch sterk, verbindend. Géén gladde sales-taal, géén AI-clichés.\nEllen's expertise: communicatietrainingen, presentatievaardigheden, DISC-profielen, train de trainer, onderwijs. Locatie: Alkmaar. Website: grootsinvertellen.nl", "projectplan": "Je bent een ervaren social media projectmanager werkend voor Ellen Groot. Maak een professioneel plan van aanpak. Structuur: 1) Doelstellingen, 2) Doelgroepanalyse, 3) Kernboodschappen, 4) Week-over-week planning, 5) KPI's. Nederlands, professioneel, concreet.", "pva": "Je bent een projectmanager die een PVA opstelt. Bevat: 1) Milestones, 2) Deliverables per fase, 3) Verantwoordelijkheden, 4) Go/No-Go momenten, 5) Risico's + mitigatie. Nederlands, professioneel.", "rollout": "Je bent een social media planner. Maak een content kalender. Output als tabel: Week | Dag | Platform | Content type | Onderwerp | Doel. Nederlands.", "leadmagnet": "Je bent een expert content creator voor Ellen Groot. Maak een praktische content. Structuur: titel, introductie, kernpunten (5-7), afsluiting met CTA naar grootsinvertellen.nl", "outreach": "Je bent Ellen Groot's outreach assistent. Schrijf warme, persoonlijke berichten. Focus op OPLOSSEN van een probleem. Empathisch, professioneel. Geen koude sales.", } @app.route("/groots/api/generate", methods=["POST"]) @rate_limit def api_generate(): try: return _api_generate() except Exception as e: import traceback; traceback.print_exc() return jsonify({"error": str(e)}), 500 def _api_generate(): data = request.get_json(force=True) print(f"API received: {data}") t = data.get("type", "") if t not in PROMPTS: return jsonify({"error": f"Ongeldig type: {t}"}), 400 sys_prompt = PROMPTS[t] theme = data.get("theme", "") keywords = data.get("keywords", "") casus = data.get("casus", "") doelgroep = data.get("doelgroep", "") platforms = [] if data.get("platform_linkedin"): platforms.append("LinkedIn") if data.get("platform_instagram"): platforms.append("Instagram") if data.get("platform_video"): platforms.append("Video") # Build user message based on type if t == "campaign": user_msg = f"""Maak een complete social media campagne. Thema: {theme} Keywords: {keywords} Casus/Context: {casus or 'Geen specifieke casus'} Doelgroep: {doelgroep} Platforms: {', '.join(platforms) or 'LinkedIn, Instagram, Video'} Genereer: - 3 LinkedIn posts (met hashtags) - 2 Instagram carrousel concepten (slide-voor-slide) - 5 video hooks (korte openingszinnen voor video's) Schrijf in de ik-vorm als Ellen Groot.""" result = call_ai(sys_prompt, user_msg, 3000) if result: try: db = get_db() db.execute("INSERT INTO campaigns (theme,keywords,casus,doelgroep,platforms,content_json) VALUES (?,?,?,?,?,?)", (theme, keywords, casus, doelgroep, ",".join(platforms), json.dumps(result))) db.commit(); db.close() except Exception as e: print(f"DB save error: {e}") return jsonify({"content": result.replace("\n", "
")}) return jsonify({"error": "AI kon geen content genereren. Probeer opnieuw."}) elif t in ("projectplan", "pva", "rollout"): weeks = data.get("duration_weeks", 4) team = data.get("team_size", 1) dg = data.get("doelgroep", "Algemeen") if t == "projectplan": user_msg = f"Maak een Plan van Aanpak voor: {theme}. Periode: {weeks} weken. Team: {team} persoon(s). Doelgroep: {dg}." elif t == "pva": user_msg = f"Maak een PVA voor: {theme}. Periode: {weeks} weken. Team: {team} persoon(s). Doelgroep: {dg}." else: user_msg = f"Maak een content kalender/rollout schema voor: {theme}. Periode: {weeks} weken. Doelgroep: {dg}." result = call_ai(sys_prompt, user_msg, 3000) if result: try: db = get_db() db.execute("INSERT INTO project_plans (theme,plan_type,duration_weeks,content) VALUES (?,?,?,?)", (theme, t, int(weeks), result)) db.commit(); db.close() except Exception as e: print(f"DB save error: {e}") return jsonify({"content": result.replace("\n", "
")}) return jsonify({"error": "AI kon geen plan genereren."}) elif t == "leadmagnet": mt = data.get("magnet_type", "checklist") topic = data.get("topic", "DISC profielen") user_msg = f"Maak een {mt} over {topic} voor Ellen Groot." result = call_ai(sys_prompt, user_msg, 2500) if result: try: db = get_db() db.execute("INSERT INTO lead_magnets (type,topic,content) VALUES (?,?,?)", (mt, topic, result)) db.commit(); db.close() except Exception as e: print(f"DB save error: {e}") return jsonify({"content": result.replace("\n", "
")}) return jsonify({"error": "AI kon geen lead magnet genereren."}) elif t == "outreach": tg = data.get("target_group", "") prob = data.get("problem", "") user_msg = f"Doelgroep: {tg}. Probleemstelling: {prob}. Schrijf 2 LinkedIn berichten en 2 e-mails." result = call_ai(sys_prompt, user_msg, 2500) if result: try: db = get_db() db.execute("INSERT INTO outreach (target_group,messages_json) VALUES (?,?)", (tg, json.dumps(result))) db.commit(); db.close() except Exception as e: print(f"DB save error: {e}") return jsonify({"content": result.replace("\n", "
")}) return jsonify({"error": "AI kon geen outreach genereren."}), 500 return jsonify({"error": "Onbekend type"}), 400 # --- Root redirect --- @app.route("/groots") def groots_root(): from flask import redirect return redirect("/groots/") if __name__ == "__main__": app.run(host="0.0.0.0", port=PORT, debug=False)