merged create and chat create in one tab

This commit is contained in:
2026-02-23 16:13:14 +01:00
parent d3ebffa811
commit 005059be84
9 changed files with 175 additions and 14 deletions

View File

@@ -80,6 +80,7 @@ class Settings(BaseSettings):
teams_enabled: bool = False
microsoft_app_id: str = ""
microsoft_app_secret: str = ""
microsoft_app_tenant_id: str = "botframework.com" # "botframework.com" for multi-tenant, tenant ID for single-tenant
model_config = SettingsConfigDict(
env_file=".env",

View File

@@ -21,9 +21,10 @@ _JWKS_TTL = 3600 # 1 hour
class TeamsService:
"""Handles Microsoft Teams bot interactions for post approval notifications."""
def __init__(self, app_id: str, app_secret: str):
def __init__(self, app_id: str, app_secret: str, tenant_id: str = "botframework.com"):
self._app_id = app_id
self._app_secret = app_secret
self._tenant_id = tenant_id
# ==================== BOT AUTH TOKEN ====================
@@ -37,7 +38,7 @@ class TeamsService:
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.post(
"https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
f"https://login.microsoftonline.com/{self._tenant_id}/oauth2/v2.0/token",
data={
"grant_type": "client_credentials",
"client_id": self._app_id,
@@ -214,4 +215,8 @@ class TeamsService:
# Module-level singleton — only created when Teams is enabled
teams_service: Optional[TeamsService] = None
if settings.teams_enabled:
teams_service = TeamsService(settings.microsoft_app_id, settings.microsoft_app_secret)
teams_service = TeamsService(
settings.microsoft_app_id,
settings.microsoft_app_secret,
settings.microsoft_app_tenant_id,
)

View File

@@ -2,9 +2,9 @@
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.gzip import GZipMiddleware
from loguru import logger
@@ -116,6 +116,82 @@ app.add_middleware(GZipMiddleware, minimum_size=500)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
def _load_env_file(path: Path) -> dict:
"""Load key=value pairs from a simple .env file."""
data = {}
if not path.exists():
return data
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
data[key.strip()] = val.strip()
return data
def _send_contact_email(payload: dict) -> bool:
"""Send contact email using landing.env SMTP config."""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
root = Path(__file__).resolve().parents[3]
env_path = root / "landing" / "landing.env"
if not env_path.exists():
env_path = root / "landing.env"
env = _load_env_file(env_path)
host = env.get("SMTP_HOST", "")
port = int(env.get("SMTP_PORT", "587") or 587)
user = env.get("SMTP_USER", "")
password = env.get("SMTP_PASSWORD", "")
from_name = env.get("SMTP_FROM_NAME", "LinkedIn Post System")
to_email = "team@onyva.de"
if not host or not user or not password:
logger.error("Contact email SMTP not configured (landing.env)")
return False
subject = "Neue Kontaktanfrage (Landing Page)"
body = (
f"Name: {payload.get('name','')}\n"
f"E-Mail: {payload.get('email','')}\n"
f"Mitarbeiter: {payload.get('team_size','')}\n"
f"Posts/Monat: {payload.get('posts_per_month','')}\n"
f"Nachricht:\n{payload.get('message','')}\n"
)
msg = MIMEMultipart()
msg["From"] = f"{from_name} <{user}>"
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
try:
with smtplib.SMTP(host, port) as server:
server.starttls()
server.login(user, password)
server.sendmail(user, to_email, msg.as_string())
return True
except Exception as exc:
logger.error(f"Contact email failed: {exc}")
return False
@app.post("/api/contact")
async def contact(request: Request):
data = await request.json()
required = ["name", "email", "team_size", "posts_per_month", "message"]
for key in required:
if not data.get(key):
return JSONResponse({"ok": False, "error": f"Missing {key}"}, status_code=400)
ok = _send_contact_email(data)
if not ok:
return JSONResponse({"ok": False, "error": "Email failed"}, status_code=500)
return {"ok": True}
@app.get("/sw.js", include_in_schema=False)
async def service_worker():
"""Serve Service Worker from root scope so it can intercept all page requests."""

View File

@@ -130,13 +130,6 @@
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
<span class="sidebar-text">Post erstellen</span>
</a>
<a href="/chat-create" class="nav-link flex items-center justify-between px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'chat-create' %}active{% endif %}">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
<span class="sidebar-text">Chat Assistent</span>
</div>
<span class="sidebar-text px-1.5 py-0.5 bg-brand-highlight text-brand-bg-dark rounded text-xs font-bold">NEU</span>
</a>
<a href="/posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'posts' %}active{% endif %}">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
<span class="sidebar-text">Meine Posts</span>

View File

@@ -233,7 +233,13 @@ aside.collapsed ~ main .chat-fixed-input {
<!-- Header (Fixed at top) -->
<div class="chat-fixed-header fixed bg-brand-bg z-20" style="top: {% if limit_reached %}1.5rem{% else %}0{% endif %}">
<div class="px-8 py-4">
<div class="px-8 py-4 flex items-center gap-4">
<a href="/create" class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-brand-highlight transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Zurück
</a>
<h1 class="text-xl font-bold text-white">💬 Chat Assistent</h1>
</div>
</div>

View File

@@ -5,6 +5,14 @@
<!-- Wizard Container (hidden during generation) -->
<div id="wizardContainer" {% if limit_reached %}style="pointer-events: none; opacity: 0.5;"{% endif %}>
<div class="max-w-2xl mx-auto">
<div class="mb-4">
<a href="/create" class="inline-flex items-center gap-2 text-gray-400 hover:text-brand-highlight transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Zurück
</a>
</div>
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
<p class="text-gray-400">Generiere einen neuen LinkedIn Post mit AI</p>

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto">
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
<p class="text-gray-400">Wähle, wie du deinen Post erstellen möchtest</p>
</div>
{% if limit_reached %}
<div class="mb-6 card-bg border rounded-xl p-4 text-center text-sm text-gray-300">
Token-Limit erreicht keine KI-Aktionen mehr heute möglich. Morgen wird das Limit zurückgesetzt.
</div>
{% endif %}
<div class="grid md:grid-cols-2 gap-6">
<a href="/create/wizard"
class="card-bg border rounded-xl p-6 hover:bg-brand-bg-light transition-colors group {% if limit_reached %}opacity-50 pointer-events-none{% endif %}">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-white mb-2">Per Wizard erstellen</h2>
<p class="text-gray-400 text-sm">Geführter Prozess mit Post-Typ, Thema und Hook-Auswahl.</p>
</a>
<a href="/chat-create"
class="card-bg border rounded-xl p-6 hover:bg-brand-bg-light transition-colors group {% if limit_reached %}opacity-50 pointer-events-none{% endif %}">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-white mb-2">Per Chat erstellen</h2>
<p class="text-gray-400 text-sm">Schnell und flexibel mit dem Chat-Assistenten.</p>
</a>
</div>
</div>
{% endblock %}

View File

@@ -1756,8 +1756,37 @@ async def research_page(request: Request):
@user_router.get("/create", response_class=HTMLResponse)
async def create_post_select_page(request: Request):
"""Post creation selection page - choose wizard or chat."""
session = require_user_session(request)
if not session:
return RedirectResponse(url="/login", status_code=302)
# Check token limit for companies/employees
limit_reached = False
limit_message = ""
if session.account_type in ("company", "employee") and session.company_id:
can_create, error_msg, _, _ = await db.check_company_token_limit(UUID(session.company_id))
limit_reached = not can_create
limit_message = error_msg
user_id = UUID(session.user_id)
profile_picture = await get_user_avatar(session, user_id)
return templates.TemplateResponse("create_post_select.html", {
"request": request,
"page": "create",
"session": session,
"user_id": session.user_id,
"limit_reached": limit_reached,
"limit_message": limit_message,
"profile_picture": profile_picture
})
@user_router.get("/create/wizard", response_class=HTMLResponse)
async def create_post_page(request: Request):
"""Create post page - with limit check for companies."""
"""Create post wizard page - with limit check for companies."""
session = require_user_session(request)
if not session:
return RedirectResponse(url="/login", status_code=302)

Binary file not shown.