simplified ui and improved hook generation and research

This commit is contained in:
2026-04-21 12:06:32 +02:00
parent 4b48a76bd5
commit 6347b53863
12 changed files with 415 additions and 459 deletions

View File

@@ -1,8 +1,10 @@
"""Research agent using Perplexity.""" """Research agent using Perplexity."""
import json import json
import random import random
import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Dict, Any, List from typing import Dict, Any, List
from urllib.parse import urlparse
from loguru import logger from loguru import logger
from src.agents.base import BaseAgent from src.agents.base import BaseAgent
@@ -11,6 +13,57 @@ from src.agents.base import BaseAgent
class ResearchAgent(BaseAgent): class ResearchAgent(BaseAgent):
"""Agent for researching new content topics using Perplexity.""" """Agent for researching new content topics using Perplexity."""
SOURCE_TIERS = {
"tier_1_primary": {
"domains": {
"destatis.de", "bundesbank.de", "bmwk.de", "bmas.de", "bmbf.de",
"europa.eu", "ec.europa.eu", "oecd.org", "weforum.org", "ilo.org",
"who.int", "worldbank.org", "imf.org", "stanford.edu", "mit.edu",
"harvard.edu", "ox.ac.uk", "cam.ac.uk", "nature.com", "science.org",
"arxiv.org", "ssrn.com", "bitkom.org", "ifo.de", "iwkoeln.de",
"iab.de", "fraunhofer.de", "acatech.de", "bertelsmann-stiftung.de"
},
"names": {
"destatis", "bundesbank", "eu-kommission", "european commission",
"oecd", "world economic forum", "stanford", "mit", "harvard",
"nature", "science", "bitkom", "ifo institut", "fraunhofer",
"institut der deutschen wirtschaft", "iab"
},
"label": "Primärquelle"
},
"tier_2_editorial": {
"domains": {
"handelsblatt.com", "faz.net", "manager-magazin.de", "wiwo.de",
"sueddeutsche.de", "zeit.de", "spiegel.de", "tagesschau.de",
"reuters.com", "bloomberg.com", "ft.com", "economist.com",
"hbr.org", "mitsloan.mit.edu", "mckinsey.com", "bcg.com",
"bain.com", "gartner.com", "forrester.com", "pwc.com",
"deloitte.com", "kpmg.com", "ey.com", "accenture.com",
"stackoverflow.blog", "stackoverflow.co", "github.blog"
},
"names": {
"handelsblatt", "frankfurter allgemeine", "faz", "manager magazin",
"wirtschaftswoche", "reuters", "bloomberg", "financial times",
"harvard business review", "mit sloan", "mckinsey", "bcg",
"bain", "gartner", "forrester", "pwc", "deloitte", "kpmg",
"accenture", "stack overflow", "github"
},
"label": "Renommierte Sekundärquelle"
},
"tier_3_vendor": {
"domains": {
"microsoft.com", "google.com", "openai.com", "anthropic.com",
"salesforce.com", "sap.com", "hubspot.com", "linkedin.com",
"workday.com", "servicenow.com", "aws.amazon.com", "cloud.google.com"
},
"names": {
"microsoft", "google", "openai", "anthropic", "salesforce",
"sap", "hubspot", "linkedin", "workday", "servicenow", "aws"
},
"label": "Unternehmens-/Vendor-Quelle"
}
}
def __init__(self): def __init__(self):
"""Initialize research agent.""" """Initialize research agent."""
super().__init__("Researcher") super().__init__("Researcher")
@@ -78,10 +131,9 @@ class ResearchAgent(BaseAgent):
"Du bist ein Research-Spezialist. Finde aktuelle Studien, Statistiken und News mit Quellenangaben." "Du bist ein Research-Spezialist. Finde aktuelle Studien, Statistiken und News mit Quellenangaben."
] ]
raw_research = await self.call_perplexity( raw_research = await self._research_with_provider(
system_prompt=random.choice(system_prompts), system_prompt=random.choice(system_prompts),
user_prompt=perplexity_prompt, user_prompt=perplexity_prompt
model="sonar-pro"
) )
logger.info("Step 2: Transforming research into personalized topic ideas") logger.info("Step 2: Transforming research into personalized topic ideas")
@@ -103,7 +155,7 @@ class ResearchAgent(BaseAgent):
system_prompt=self._get_topic_creator_system_prompt(), system_prompt=self._get_topic_creator_system_prompt(),
user_prompt=transform_prompt, user_prompt=transform_prompt,
model="gpt-4o", model="gpt-4o",
temperature=0.7, # Higher for creative topic angles temperature=0.35,
response_format={"type": "json_object"} response_format={"type": "json_object"}
) )
@@ -111,7 +163,15 @@ class ResearchAgent(BaseAgent):
result = json.loads(response) result = json.loads(response)
suggested_topics = result.get("topics", []) suggested_topics = result.get("topics", [])
# STEP 3: Ensure diversity - filter out similar topics # STEP 3: Quality gate + diversity
suggested_topics = self._quality_gate_topics(
suggested_topics,
persona=persona,
content_pillars=content_pillars,
target_audience=target_audience,
existing_topics=existing_topics,
example_posts=example_posts or []
)
suggested_topics = self._ensure_diversity(suggested_topics) suggested_topics = self._ensure_diversity(suggested_topics)
# Parse research results # Parse research results
@@ -125,6 +185,18 @@ class ResearchAgent(BaseAgent):
logger.info(f"Research completed with {len(research_results['suggested_topics'])} topic suggestions") logger.info(f"Research completed with {len(research_results['suggested_topics'])} topic suggestions")
return research_results return research_results
async def _research_with_provider(self, system_prompt: str, user_prompt: str) -> str:
"""Run live web research through the current provider.
This method intentionally keeps Perplexity isolated so the provider can be
swapped for Azure/Exa/Tavily/Brave without rewriting topic transformation.
"""
return await self.call_perplexity(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="sonar-pro"
)
def _get_topic_creator_system_prompt(self) -> str: def _get_topic_creator_system_prompt(self) -> str:
"""Get system prompt for transforming research into personalized topics.""" """Get system prompt for transforming research into personalized topics."""
return """Du bist ein LinkedIn Content-Stratege, der aus Recherche-Ergebnissen KONKRETE, PERSONALISIERTE Themenvorschläge erstellt. return """Du bist ein LinkedIn Content-Stratege, der aus Recherche-Ergebnissen KONKRETE, PERSONALISIERTE Themenvorschläge erstellt.
@@ -151,6 +223,8 @@ Deine Themenvorschläge müssen:
3. EINEN HOOK VORSCHLAGEN - Eine konkrete Idee wie der Post starten könnte 3. EINEN HOOK VORSCHLAGEN - Eine konkrete Idee wie der Post starten könnte
4. HINTERGRUND-INFOS LIEFERN - Fakten/Daten aus der Recherche die die Person nutzen kann 4. HINTERGRUND-INFOS LIEFERN - Fakten/Daten aus der Recherche die die Person nutzen kann
5. ABWECHSLUNGSREICH SEIN - Verschiedene Formate und Kategorien 5. ABWECHSLUNGSREICH SEIN - Verschiedene Formate und Kategorien
6. QUELLENTREU BLEIBEN - Nur Fakten aus den Recherche-Ergebnissen verwenden, keine Zahlen erfinden
7. REDAKTIONELL FILTERN - Wenn Quelle, Datum oder Fakt schwach sind, keinen Themenvorschlag daraus machen
Antworte als JSON.""" Antworte als JSON."""
@@ -272,6 +346,8 @@ Gib deine Antwort im folgenden JSON-Format zurück:
if persona: if persona:
persona_hint = f"\nEXPERTISE DER PERSON: {persona[:600]}\n" persona_hint = f"\nEXPERTISE DER PERSON: {persona[:600]}\n"
trusted_sources = self._trusted_sources_prompt()
# Randomize the research focus for variety # Randomize the research focus for variety
research_angles = [ research_angles = [
{ {
@@ -335,12 +411,15 @@ KONTEXT:
RECHERCHE-SCHWERPUNKTE FÜR DIESE SESSION: RECHERCHE-SCHWERPUNKTE FÜR DIESE SESSION:
{angles_text} {angles_text}
BEVORZUGTE RENOMMIERTE QUELLEN:
{trusted_sources}
⛔ BEREITS BEHANDELTE THEMEN - NICHT NOCHMAL VORSCHLAGEN: ⛔ BEREITS BEHANDELTE THEMEN - NICHT NOCHMAL VORSCHLAGEN:
{existing_text} {existing_text}
=== DEINE AUFGABE === === DEINE AUFGABE ===
Recherchiere FAKTEN, DATEN und ENTWICKLUNGEN - keine fertigen Themenvorschläge! Recherchiere FAKTEN, DATEN und ENTWICKLUNGEN aus renommierten Quellen - keine fertigen Themenvorschläge!
Ich brauche ROHDATEN die ich dann in personalisierte Content-Ideen umwandeln kann. Ich brauche ROHDATEN die ich dann in personalisierte Content-Ideen umwandeln kann.
Für jede Entwicklung/News sammle: Für jede Entwicklung/News sammle:
@@ -348,8 +427,9 @@ Für jede Entwicklung/News sammle:
2. **Zahlen & Daten** - Statistiken, Prozentsätze, Beträge, Veränderungen 2. **Zahlen & Daten** - Statistiken, Prozentsätze, Beträge, Veränderungen
3. **Wer ist beteiligt?** - Unternehmen, Personen, Organisationen 3. **Wer ist beteiligt?** - Unternehmen, Personen, Organisationen
4. **Wann?** - Genaues Datum oder Zeitraum 4. **Wann?** - Genaues Datum oder Zeitraum
5. **Quelle** - URL oder Publikationsname 5. **Quelle** - URL, Publikationsname und falls möglich Autor/Organisation
6. **Kontext** - Warum ist das relevant? Was bedeutet es? 6. **Kontext** - Warum ist das relevant? Was bedeutet es?
7. **Quellenqualität** - Primärquelle, renommierte Sekundärquelle oder Vendor/Unternehmensquelle
SUCHE NACH: SUCHE NACH:
✅ Neue Studien/Reports mit konkreten Zahlen ✅ Neue Studien/Reports mit konkreten Zahlen
@@ -364,8 +444,9 @@ FORMAT DEINER ANTWORT:
Liefere 8-10 verschiedene Entwicklungen/News mit möglichst vielen Fakten und Zahlen. Liefere 8-10 verschiedene Entwicklungen/News mit möglichst vielen Fakten und Zahlen.
Formatiere sie klar und strukturiert. Formatiere sie klar und strukturiert.
QUALITÄTSKRITERIEN: QUALITÄTSKRITERIEN (streng):
AKTUALITÄT: Von dieser Woche oder letzter Woche QUELLE: Bevorzuge Primärquellen, offizielle Reports, renommierte Wirtschafts-/Fachmedien
✅ AKTUALITÄT: Möglichst letzte 30 Tage; bei Studien/Reports maximal 12 Monate, wenn sie substanziell sind
✅ KONKRETHEIT: Echte Zahlen, Namen, Daten (nicht "Experten sagen...") ✅ KONKRETHEIT: Echte Zahlen, Namen, Daten (nicht "Experten sagen...")
✅ VERIFIZIERBARKEIT: Echte Quelle die man prüfen kann ✅ VERIFIZIERBARKEIT: Echte Quelle die man prüfen kann
✅ BRANCHENRELEVANZ: Spezifisch für {industry} ✅ BRANCHENRELEVANZ: Spezifisch für {industry}
@@ -374,7 +455,9 @@ QUALITÄTSKRITERIEN:
- Vage Aussagen ohne Daten ("KI wird wichtiger") - Vage Aussagen ohne Daten ("KI wird wichtiger")
- Generische Trends ohne konkreten Aufhänger - Generische Trends ohne konkreten Aufhänger
- Alte News die jeder schon kennt - Alte News die jeder schon kennt
- Themen ohne verifizierbare Fakten""" - Themen ohne verifizierbare Fakten
- SEO-Listen, Content-Farmen, reine Meinungsblogs ohne Primärdaten
- Quellen, die nur andere Quellen zusammenfassen und keinen eigenen Beleg liefern"""
def _get_transform_prompt( def _get_transform_prompt(
self, self,
@@ -479,9 +562,17 @@ FORMAT DER THEMENVORSCHLÄGE:
"category": "Meinung/Take | Erfahrungsbericht | Konträr | How-To/Insight | Story | Analyse", "category": "Meinung/Take | Erfahrungsbericht | Konträr | How-To/Insight | Story | Analyse",
"angle": "Der spezifische Blickwinkel/die Perspektive für diesen Post", "angle": "Der spezifische Blickwinkel/die Perspektive für diesen Post",
"hook_idea": "Konkrete Hook-Idee die zum Post passen würde (1-2 Sätze)", "hook_idea": "Konkrete Hook-Idee die zum Post passen würde (1-2 Sätze)",
"fact": "Kurzer, konkreter Kernfakt für die bestehende Topic-Auswahl in der UI",
"relevance": "Warum ist das für die Zielgruppe relevant?",
"key_facts": ["Fakt 1 aus der Recherche", "Fakt 2 mit Zahlen", "Fakt 3"], "key_facts": ["Fakt 1 aus der Recherche", "Fakt 2 mit Zahlen", "Fakt 3"],
"why_this_person": "Warum passt dieses Thema zu DIESER Person und ihrer Expertise?", "why_this_person": "Warum passt dieses Thema zu DIESER Person und ihrer Expertise?",
"source": "Quellenangabe" "source": "Konkrete Quellenangabe mit URL oder Publikationsname",
"source_title": "Titel der Quelle / Studie / Meldung",
"source_domain": "Domain falls URL vorhanden",
"published_at": "Publikationsdatum oder Zeitraum falls bekannt",
"evidence": ["Beleg 1 aus Quelle", "Beleg 2 aus Quelle"],
"source_quality": "Primärquelle | Renommierte Sekundärquelle | Vendor/Unternehmensquelle",
"fit_reason": "Warum ist der Angle für diese Person glaubwürdig?"
}} }}
] ]
}} }}
@@ -492,16 +583,220 @@ BEISPIEL EINES GUTEN THEMENVORSCHLAGS:
"category": "Erfahrungsbericht", "category": "Erfahrungsbericht",
"angle": "Persönliche Erfahrung eines Tech-Leads mit der Veränderung seiner Rolle durch KI", "angle": "Persönliche Erfahrung eines Tech-Leads mit der Veränderung seiner Rolle durch KI",
"hook_idea": "Vor einem Jahr habe ich Code geschrieben. Heute schreibe ich Prompts. Und ehrlich? Ich weiß noch nicht ob das gut oder schlecht ist.", "hook_idea": "Vor einem Jahr habe ich Code geschrieben. Heute schreibe ich Prompts. Und ehrlich? Ich weiß noch nicht ob das gut oder schlecht ist.",
"fact": "KI-Tools verändern Entwicklungsarbeit messbar: viele Teams sparen Zeit beim Coden, investieren aber mehr in Review und Qualitätssicherung.",
"relevance": "Für technische Führungskräfte ist nicht nur Produktivität relevant, sondern wie sich Verantwortung, Review-Prozesse und Teamrollen verändern.",
"key_facts": ["GitHub Copilot wird von 92% der Entwickler genutzt (Stack Overflow 2024)", "Durchschnittliche Zeitersparnis: 55%", "Aber: Code-Review-Zeit +40%"], "key_facts": ["GitHub Copilot wird von 92% der Entwickler genutzt (Stack Overflow 2024)", "Durchschnittliche Zeitersparnis: 55%", "Aber: Code-Review-Zeit +40%"],
"why_this_person": "Als Tech-Lead hat die Person direkten Einblick in diese Veränderung und kann authentisch darüber berichten", "why_this_person": "Als Tech-Lead hat die Person direkten Einblick in diese Veränderung und kann authentisch darüber berichten",
"source": "Stack Overflow Developer Survey 2024" "source": "Stack Overflow Developer Survey 2024",
"source_title": "Stack Overflow Developer Survey 2024",
"source_domain": "stackoverflow.co",
"published_at": "2024",
"evidence": ["92% der Entwickler nutzen KI-Tools oder planen es", "Konkrete Aussagen zur Nutzung von KI im Entwicklungsworkflow"],
"source_quality": "Primärquelle",
"fit_reason": "Direkter Bezug zu Rolle, Teamalltag und technischen Entscheidungen"
}} }}
WICHTIG: WICHTIG:
- Jeder Vorschlag muss sich UNTERSCHEIDEN (anderer Angle, andere Kategorie) - Jeder Vorschlag muss sich UNTERSCHEIDEN (anderer Angle, andere Kategorie)
- Keine generischen "Die Zukunft von X" Themen - Keine generischen "Die Zukunft von X" Themen
- Hook-Ideen müssen zum Stil der Beispiel-Posts passen! - Hook-Ideen müssen zum Stil der Beispiel-Posts passen!
- Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)""" - Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)
- Verwirf Rohdaten mit schwacher Quelle statt sie kreativ umzudeuten
- Jeder Vorschlag braucht eine erkennbare, konkrete Quelle"""
def _trusted_sources_prompt(self) -> str:
"""Return a compact source policy for the research provider prompt."""
return """Tier 1 bevorzugen: offizielle Reports/Studien/Primärquellen (Destatis, Bundesbank, EU/OECD/WEF/ILO/World Bank/IMF, Universitäten, Fraunhofer, Ifo, Bitkom, Stack Overflow Survey, Unternehmensberichte).
Tier 2 zulässig: renommierte Wirtschafts- und Fachmedien oder Beratungs-/Researchhäuser (Handelsblatt, FAZ, Manager Magazin, WiWo, Reuters, Bloomberg, FT, HBR, MIT Sloan, McKinsey, BCG, Gartner, Forrester, Deloitte, PwC).
Tier 3 nur mit Vorsicht: Vendor-/Unternehmensblogs, wenn sie Primärdaten, Produktankündigungen oder konkrete Reports enthalten.
Nicht verwenden: SEO-Content, generische Trendlisten, Content-Farmen, reine Meinungsblogs ohne überprüfbare Daten."""
def _extract_domain(self, source: str) -> str:
"""Extract a normalized domain from a source string or URL."""
if not source:
return ""
urls = re.findall(r"https?://[^\s),\]]+", source)
candidate = urls[0] if urls else source.strip().split()[0]
parsed = urlparse(candidate if "://" in candidate else f"https://{candidate}")
domain = (parsed.netloc or parsed.path).lower().strip("/")
if domain.startswith("www."):
domain = domain[4:]
return domain
def _domain_matches(self, domain: str, configured_domain: str) -> bool:
configured_domain = configured_domain.lower()
return domain == configured_domain or domain.endswith(f".{configured_domain}")
def _classify_source(self, topic: Dict[str, Any]) -> Dict[str, str]:
"""Classify the source into a source tier using URL/domain and publication names."""
source_text = " ".join([
str(topic.get("source", "") or ""),
str(topic.get("source_title", "") or ""),
str(topic.get("source_domain", "") or ""),
]).lower()
domain = self._extract_domain(source_text)
for tier, config in self.SOURCE_TIERS.items():
for configured_domain in config["domains"]:
if domain and self._domain_matches(domain, configured_domain):
return {"source_tier": tier, "source_domain": domain, "source_quality": config["label"]}
for name in config["names"]:
if name in source_text:
return {"source_tier": tier, "source_domain": domain, "source_quality": config["label"]}
return {
"source_tier": "unverified",
"source_domain": domain,
"source_quality": "Nicht klassifizierte Quelle"
}
def _keyword_set(self, values: List[Any], limit: int = 80) -> set:
"""Extract a compact set of meaningful keywords for heuristics."""
text_parts: List[str] = []
for value in values:
if isinstance(value, str):
text_parts.append(value)
elif isinstance(value, list):
text_parts.extend([str(v) for v in value if v])
text = " ".join(text_parts).lower()
words = re.findall(r"\b[a-zA-ZäöüÄÖÜß][a-zA-ZäöüÄÖÜß\-]{3,}\b", text)
stop_words = {
"dass", "oder", "aber", "eine", "einer", "einem", "einen", "diese", "dieser",
"dieses", "nicht", "auch", "mehr", "über", "unter", "durch", "thema", "werden",
"wurde", "sind", "kann", "sich", "their", "about", "with", "from", "this",
"that", "into", "professionals", "business"
}
result = []
seen = set()
for word in words:
if word in stop_words or word in seen:
continue
seen.add(word)
result.append(word)
if len(result) >= limit:
break
return set(result)
def _score_topic_quality(
self,
topic: Dict[str, Any],
persona_keywords: set,
existing_keywords: set
) -> float:
"""Score a topic for source quality, evidence, specificity and person fit."""
score = 0.0
tier = topic.get("source_tier", "unverified")
if tier == "tier_1_primary":
score += 5.0
elif tier == "tier_2_editorial":
score += 4.0
elif tier == "tier_3_vendor":
score += 2.0
else:
score -= 2.0
text_values = [
topic.get("title", ""),
topic.get("angle", ""),
topic.get("fact", ""),
topic.get("why_this_person", ""),
topic.get("fit_reason", ""),
topic.get("relevance", ""),
topic.get("key_facts", []),
topic.get("evidence", []),
]
topic_keywords = self._keyword_set(text_values, limit=80)
overlap = topic_keywords.intersection(persona_keywords)
score += min(len(overlap), 6) * 0.7
if topic.get("why_this_person") or topic.get("fit_reason"):
score += 1.5
if topic.get("angle"):
score += 1.0
if topic.get("evidence") and isinstance(topic.get("evidence"), list):
score += min(len(topic["evidence"]), 3) * 0.8
if topic.get("key_facts") and isinstance(topic.get("key_facts"), list):
score += min(len(topic["key_facts"]), 3) * 0.6
if re.search(r"\d", " ".join([str(v) for v in text_values])):
score += 1.0
if not topic.get("source"):
score -= 4.0
if len(topic_keywords.intersection(existing_keywords)) >= 5:
score -= 2.0
return round(score, 2)
def _quality_gate_topics(
self,
topics: List[Dict[str, Any]],
persona: str,
content_pillars: List[str],
target_audience: str,
existing_topics: List[str],
example_posts: List[str]
) -> List[Dict[str, Any]]:
"""Normalize, score and filter researched topics before they reach the UI."""
if not isinstance(topics, list):
return []
persona_keywords = self._keyword_set(
[persona, content_pillars, target_audience, example_posts[:5]],
limit=120
)
existing_keywords = self._keyword_set(existing_topics[:30], limit=160)
processed: List[Dict[str, Any]] = []
seen_titles = set()
for topic in topics:
if not isinstance(topic, dict):
continue
title = str(topic.get("title", "") or "").strip()
if not title:
continue
title_key = re.sub(r"\s+", " ", title.lower())
if title_key in seen_titles:
continue
seen_titles.add(title_key)
source_info = self._classify_source(topic)
normalized = dict(topic)
normalized.update({k: v for k, v in source_info.items() if v})
if not normalized.get("fact") and isinstance(normalized.get("key_facts"), list) and normalized["key_facts"]:
normalized["fact"] = str(normalized["key_facts"][0])
if not normalized.get("source_quality") or normalized.get("source_quality") == "Nicht klassifizierte Quelle":
normalized["source_quality"] = source_info["source_quality"]
if not normalized.get("source_domain") and source_info.get("source_domain"):
normalized["source_domain"] = source_info["source_domain"]
quality_score = self._score_topic_quality(normalized, persona_keywords, existing_keywords)
normalized["research_quality_score"] = quality_score
normalized["research_provider"] = "perplexity"
# Keep strong unknown-source candidates only if they are highly specific and person-fit.
if quality_score >= 3.0:
processed.append(normalized)
if len(processed) < 4:
logger.warning("Research quality gate found fewer than 4 strong topics; relaxing threshold.")
relaxed = []
for topic in topics:
if not isinstance(topic, dict) or not topic.get("title"):
continue
source_info = self._classify_source(topic)
normalized = dict(topic)
normalized.update({k: v for k, v in source_info.items() if v})
if not normalized.get("fact") and isinstance(normalized.get("key_facts"), list) and normalized["key_facts"]:
normalized["fact"] = str(normalized["key_facts"][0])
normalized["research_quality_score"] = self._score_topic_quality(
normalized, persona_keywords, existing_keywords
)
normalized["research_provider"] = "perplexity"
relaxed.append(normalized)
processed = sorted(relaxed, key=lambda t: t.get("research_quality_score", 0), reverse=True)[:6]
processed.sort(key=lambda t: t.get("research_quality_score", 0), reverse=True)
logger.info(f"Research quality gate: {len(topics)} -> {len(processed)} topics")
return processed[:8]
def _get_strategy_section(self, company_strategy: Dict[str, Any] = None, strategy_weight: float = 0.5) -> str: def _get_strategy_section(self, company_strategy: Dict[str, Any] = None, strategy_weight: float = 0.5) -> str:
"""Build company strategy section for research prompt with weighted influence.""" """Build company strategy section for research prompt with weighted influence."""

View File

@@ -1352,7 +1352,7 @@ Gib NUR den fertigen Post zurück."""
post_type: Any = None post_type: Any = None
) -> List[Dict[str, str]]: ) -> List[Dict[str, str]]:
""" """
Generate 4 different hook options for a topic. Generate 4 high-quality hook options for a topic.
Args: Args:
topic: Topic dictionary with title, fact, etc. topic: Topic dictionary with title, fact, etc.
@@ -1374,11 +1374,21 @@ Gib NUR den fertigen Post zurück."""
# Build example hooks section # Build example hooks section
example_hooks = "" example_hooks = ""
if hook_phrases: if hook_phrases:
example_hooks = "\n\nBeispiel-Hooks dieser Person (zur Inspiration):\n" + "\n".join([f"- {h}" for h in hook_phrases[:5]]) example_hooks = "\n\nBeispiel-Hooks dieser Person (Muster erkennen, NICHT kopieren):\n" + "\n".join([f"- {h}" for h in hook_phrases[:8]])
anchor_terms = self._extract_hook_anchor_terms(topic, user_thoughts) anchor_terms = self._extract_hook_anchor_terms(topic, user_thoughts)
anchor_terms_text = ", ".join(anchor_terms[:10]) if anchor_terms else "keine" anchor_terms_text = ", ".join(anchor_terms[:10]) if anchor_terms else "keine"
format_card = self._build_format_card(profile_analysis) format_card = self._build_format_card(profile_analysis)
source_context = ""
if topic.get("source") or topic.get("source_title") or topic.get("source_quality"):
source_context = f"""
QUELLENKONTEXT:
- Quelle: {topic.get('source_title') or topic.get('source') or 'unbekannt'}
- Quellenqualität: {topic.get('source_quality') or topic.get('source_tier') or 'nicht klassifiziert'}
- Datum/Zeitraum: {topic.get('published_at') or 'nicht angegeben'}
"""
person_fit = topic.get("why_this_person") or topic.get("fit_reason") or topic.get("relevance") or ""
system_prompt = f"""Du bist ein Hook-Spezialist für LinkedIn Posts. Deine Aufgabe ist es, HOCHWERTIGE Hooks zu generieren, die konkret, sinnvoll und stiltreu sind. system_prompt = f"""Du bist ein Hook-Spezialist für LinkedIn Posts. Deine Aufgabe ist es, HOCHWERTIGE Hooks zu generieren, die konkret, sinnvoll und stiltreu sind.
@@ -1388,42 +1398,43 @@ STIL DER PERSON:
- Ansprache: {address} - Ansprache: {address}
{example_hooks} {example_hooks}
{format_card} {format_card}
{source_context}
NICHT VERHANDELBAR: NICHT VERHANDELBAR:
- Jeder Hook muss auf dem tatsächlichen Inhalt basieren, nicht auf leerer Aufmerksamkeit. - Jeder Hook muss auf dem tatsächlichen Inhalt basieren, nicht auf leerer Aufmerksamkeit.
- Kein generischer Einstieg. Kein austauschbares "Die meisten unterschätzen...", "Was wäre wenn...", "X verändert alles", wenn das nicht konkret begründet ist. - Kein generischer Einstieg. Kein austauschbares "Die meisten unterschätzen...", "Was wäre wenn...", "X verändert alles", wenn das nicht konkret begründet ist.
- Keine leeren LinkedIn-Templates wie "Niemand spricht darüber", "Unpopular opinion", "Ich muss ehrlich sein", "Das verändert alles".
- Kein Cliffhanger ohne klare inhaltliche Verbindung. - Kein Cliffhanger ohne klare inhaltliche Verbindung.
- Kein Fakt behaupten, der im Input nicht enthalten ist. - Kein Fakt behaupten, der im Input nicht enthalten ist.
- Wenn ein Hook provokant ist, muss die Provokation logisch aus dem Thema ableitbar sein. - Wenn ein Hook provokant ist, muss die Provokation logisch aus dem Thema ableitbar sein.
- Wenn ein Hook storytelling ist, muss er wie ein glaubwürdiger Einstieg in die Perspektive der Person wirken. - Wenn ein Hook storytelling ist, muss er wie ein glaubwürdiger Einstieg in die Perspektive der Person wirken.
- Der Hook muss auch OHNE den Rest des Posts Sinn ergeben. - Der Hook muss auch OHNE den Rest des Posts Sinn ergeben.
- Hook darf nicht klingen wie eine Überschrift, sondern wie ein echter erster Satz der Person.
- Wenn die Person eher sachlich schreibt, keine künstliche Dramatisierung.
GENERIERE 6 VERSCHIEDENE HOOKS: GENERIERE 12 VERSCHIEDENE HOOK-KANDIDATEN:
1. **Provokant** - Eine kontroverse These oder überraschende Aussage - 2x Pointierte Beobachtung
2. **Storytelling** - Beginn einer persönlichen Geschichte oder Anekdote - 2x Konkreter Daten-/Quellenanker
3. **Fakten-basiert** - Eine überraschende Statistik oder Fakt - 2x Persönliche Einordnung
4. **Neugier-weckend** - Eine Frage oder ein Cliffhanger - 2x Konträrer Take
5. **Pointiert** - Eine präzise, starke Beobachtung - 2x Problem-/Spannungsbogen
6. **Reflektiert** - Eine nachdenkliche, glaubwürdige Einordnung - 2x Ruhige Expertenbeobachtung
REGELN: REGELN:
- Jeder Hook sollte 1-2 Sätze lang sein - Jeder Hook sollte 1-2 Sätze lang sein
- Hooks müssen zum Energie-Level und Ton der Person passen - Hooks müssen zum Energie-Level und Ton der Person passen
- Keine KI-typischen Phrasen ("In der heutigen Zeit", "Stellen Sie sich vor") - Keine KI-typischen Phrasen ("In der heutigen Zeit", "Stellen Sie sich vor")
- Die Hooks sollen SOFORT Aufmerksamkeit erregen, aber nur mit echter Substanz - Die Hooks sollen Aufmerksamkeit erzeugen, aber nur mit echter Substanz
- Bei Energie 8+ darf es emotional/leidenschaftlich sein - Bei Energie 8+ darf es emotional/leidenschaftlich sein
- Bei Energie 5-7 eher sachlich-professionell - Bei Energie 5-7 eher sachlich-professionell
- Nutze nach Möglichkeit mindestens einen dieser inhaltlichen Anker: {anchor_terms_text} - Nutze nach Möglichkeit mindestens einen dieser inhaltlichen Anker: {anchor_terms_text}
- Nutze bevorzugt einen konkreten Fakt, ein klares Spannungsfeld oder die glaubwürdige Perspektive der Person
- Wenn Hook-Fettung laut Format Card erforderlich ist, formatiere nur den Hook selbst passend. Kein Markdown. - Wenn Hook-Fettung laut Format Card erforderlich ist, formatiere nur den Hook selbst passend. Kein Markdown.
Antworte im JSON-Format: Antworte im JSON-Format:
{{"hooks": [ {{"hooks": [
{{"hook": "Der Hook-Text hier", "style": "Provokant", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}}, {{"hook": "Der Hook-Text hier", "style": "Pointiert", "anchor": "konkreter Fakt/Gedanke dahinter", "fit": "warum passt das zur Person"}},
{{"hook": "Der Hook-Text hier", "style": "Storytelling", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}}, {{"hook": "Der Hook-Text hier", "style": "Datenanker", "anchor": "konkreter Fakt/Gedanke dahinter", "fit": "warum passt das zur Person"}}
{{"hook": "Der Hook-Text hier", "style": "Fakten-basiert", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}},
{{"hook": "Der Hook-Text hier", "style": "Neugier-weckend", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}},
{{"hook": "Der Hook-Text hier", "style": "Pointiert", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}},
{{"hook": "Der Hook-Text hier", "style": "Reflektiert", "anchor": "welcher konkrete Fakt oder Gedanke dahinter steckt"}}
]}}""" ]}}"""
# Build user prompt # Build user prompt
@@ -1438,6 +1449,10 @@ Antworte im JSON-Format:
post_type_section += f"\n{post_type.description}" post_type_section += f"\n{post_type.description}"
content_block = topic.get('fact', topic.get('description', 'Keine Details verfügbar')) content_block = topic.get('fact', topic.get('description', 'Keine Details verfügbar'))
if topic.get('angle'):
content_block += f"\n\nANGLE:\n{topic.get('angle')}"
if person_fit:
content_block += f"\n\nWARUM DIESE PERSON:\n{person_fit}"
if topic.get('summary'): if topic.get('summary'):
content_block += f"\n\nZUSAMMENFASSUNG:\n{topic.get('summary')}" content_block += f"\n\nZUSAMMENFASSUNG:\n{topic.get('summary')}"
if topic.get('extended_summary'): if topic.get('extended_summary'):
@@ -1446,8 +1461,12 @@ Antworte im JSON-Format:
content_block += "\n\nGLIEDERUNG:\n" + "\n".join([f"- {o}" for o in topic.get('outline', [])]) content_block += "\n\nGLIEDERUNG:\n" + "\n".join([f"- {o}" for o in topic.get('outline', [])])
if topic.get('key_points') and isinstance(topic.get('key_points'), list): if topic.get('key_points') and isinstance(topic.get('key_points'), list):
content_block += "\n\nKERNPUNKTE:\n" + "\n".join([f"- {p}" for p in topic.get('key_points', [])]) content_block += "\n\nKERNPUNKTE:\n" + "\n".join([f"- {p}" for p in topic.get('key_points', [])])
if topic.get('key_facts') and isinstance(topic.get('key_facts'), list):
content_block += "\n\nKEY FACTS:\n" + "\n".join([f"- {p}" for p in topic.get('key_facts', [])])
if topic.get('evidence') and isinstance(topic.get('evidence'), list):
content_block += "\n\nBELEGE:\n" + "\n".join([f"- {p}" for p in topic.get('evidence', [])])
user_prompt = f"""Generiere 6 Hooks für dieses Thema: user_prompt = f"""Generiere 12 Hook-Kandidaten für dieses Thema:
THEMA: {topic.get('title', 'Unbekanntes Thema')} THEMA: {topic.get('title', 'Unbekanntes Thema')}
@@ -1460,7 +1479,9 @@ KERN-FAKT/INHALT:
WICHTIGE ANKERBEGRIFFE: WICHTIGE ANKERBEGRIFFE:
{anchor_terms_text} {anchor_terms_text}
Generiere jetzt die 6 verschiedenen Hooks im JSON-Format.""" Wähle keine generischen Einstiege. Jeder Kandidat muss an einem konkreten Fakt, Angle oder persönlichen Gedanken hängen.
Generiere jetzt die 12 verschiedenen Hooks im JSON-Format."""
response = await self.call_azure_claude( response = await self.call_azure_claude(
system_prompt=system_prompt, system_prompt=system_prompt,
@@ -1541,13 +1562,25 @@ Generiere jetzt die 6 verschiedenen Hooks im JSON-Format."""
generic_penalties = [ generic_penalties = [
"die meisten unterschätzen", "die meisten unterschätzen",
"viele unterschätzen",
"was wäre wenn", "was wäre wenn",
"verändert alles", "verändert alles",
"niemand spricht darüber", "niemand spricht darüber",
"unpopular opinion",
"unpopuläre meinung",
"ich muss ehrlich sein",
"ehrlich gesagt",
"lass uns", "lass uns",
"stellen sie sich vor", "stellen sie sich vor",
"stell dir vor",
"in der heutigen zeit", "in der heutigen zeit",
"es ist kein geheimnis", "es ist kein geheimnis",
"gamechanger",
"game changer",
"revolutioniert",
"die zukunft von",
"wir müssen reden",
"hier ist warum",
] ]
for phrase in generic_penalties: for phrase in generic_penalties:
if phrase in hook_lower: if phrase in hook_lower:
@@ -1559,16 +1592,29 @@ Generiere jetzt die 6 verschiedenen Hooks im JSON-Format."""
overlap += 1 overlap += 1
score += min(overlap, 3) * 2.5 score += min(overlap, 3) * 2.5
concrete_signals = [
bool(re.search(r"\d", hook)),
any(char in hook for char in [":", "", "-"]),
len(re.findall(r"\b[A-ZÄÖÜ][a-zäöüß]{3,}\b", hook)) >= 1,
]
score += sum(concrete_signals) * 0.6
if "?" in hook: if "?" in hook:
score += 0.5 score += 0.2
if len(hook) < 18: if len(hook) < 18:
score -= 2.0 score -= 2.0
if len(hook) > 220: if len(hook) > 180:
score -= 2.0 score -= 2.0
if hook.count(".") > 2: if hook.count(".") > 2:
score -= 1.5 score -= 1.5
if hook_lower.count("ich") > 2:
score -= 0.8
if style in {"Provokant", "Storytelling", "Fakten-basiert", "Neugier-weckend", "Pointiert", "Reflektiert"}: if style in {
"Provokant", "Storytelling", "Fakten-basiert", "Neugier-weckend",
"Pointiert", "Reflektiert", "Datenanker", "Persönlich",
"Konträr", "Problem", "Expertenbeobachtung"
}:
score += 0.5 score += 0.5
return score return score
@@ -1599,7 +1645,25 @@ Generiere jetzt die 6 verschiedenen Hooks im JSON-Format."""
scored.append({"hook": hook, "style": style, "score": score}) scored.append({"hook": hook, "style": style, "score": score})
scored.sort(key=lambda x: x["score"], reverse=True) scored.sort(key=lambda x: x["score"], reverse=True)
return [{"hook": item["hook"], "style": item["style"]} for item in scored[:4]]
selected: List[Dict[str, Any]] = []
used_styles = set()
for item in scored:
if len(selected) >= 4:
break
if item["style"] in used_styles and len(scored) >= 6:
continue
used_styles.add(item["style"])
selected.append(item)
if len(selected) < 4:
for item in scored:
if item not in selected:
selected.append(item)
if len(selected) >= 4:
break
return [{"hook": item["hook"], "style": item["style"]} for item in selected[:4]]
async def generate_improvement_suggestions( async def generate_improvement_suggestions(
self, self,

View File

@@ -719,10 +719,9 @@ function showFinalResult(result) {
resultHeader.innerHTML = ` resultHeader.innerHTML = `
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}"> <span class="px-3 py-1.5 rounded-lg text-sm font-medium bg-green-600/30 text-green-300">
${result.approved ? 'Approved' : 'Review needed'} Post erstellt
</span> </span>
<span class="text-gray-400">Score: <span class="text-white font-medium">${result.final_score}/100</span></span>
<span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span> <span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span>
</div> </div>
`; `;
@@ -801,44 +800,20 @@ function renderVersions(versions, feedbackList) {
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}"> ${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1} V${i + 1}
${feedbackList[i] ? `<span class="ml-1 opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button> </button>
`).join(''); `).join('');
const currentVersion = versions[currentVersionIndex]; const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
versionsContainer.innerHTML = ` versionsContainer.innerHTML = `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-6"> <div class="grid grid-cols-1 gap-6">
<div class="bg-brand-bg/50 rounded-lg p-5"> <div class="bg-brand-bg/50 rounded-lg p-5">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span> <span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? ` <span class="text-xs text-gray-500">Wird erstellt...</span>
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div> </div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre> <pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre>
</div> </div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-5 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-3">Kritik</span>
<p class="text-sm text-gray-400 mb-4">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div>
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-2 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-2">
<span class="text-yellow-500 mt-0.5">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div> </div>
`; `;
} }

View File

@@ -115,9 +115,6 @@
<h1 class="text-2xl font-bold text-white mb-2">{{ post.topic_title or 'Untitled Post' }}</h1> <h1 class="text-2xl font-bold text-white mb-2">{{ post.topic_title or 'Untitled Post' }}</h1>
<div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap"> <div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap">
<span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span> <span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span>
<span class="text-gray-600">|</span>
<span>{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}</span>
<span class="text-gray-600">|</span>
<span>{{ employee_name }}</span> <span>{{ employee_name }}</span>
</div> </div>
</div> </div>
@@ -365,20 +362,6 @@
</div> </div>
{% endif %} {% endif %}
<!-- Style Card -->
{% if style_card %}
<div class="section-card rounded-xl p-6">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h10M7 16h6"/></svg>
Style Card
</h3>
<span class="text-xs text-gray-500">Quelle: Profilanalyse</span>
</div>
<pre class="whitespace-pre-wrap text-gray-300 text-xs bg-brand-bg/50 rounded-lg p-4 border border-brand-bg-light">{{ style_card }}</pre>
</div>
{% endif %}
<!-- Schedule Section --> <!-- Schedule Section -->
{% if not permissions or permissions.can_schedule_posts %} {% if not permissions or permissions.can_schedule_posts %}
<div class="section-card rounded-xl p-6"> <div class="section-card rounded-xl p-6">
@@ -481,10 +464,6 @@
<span class="text-gray-400">Erstellt</span> <span class="text-gray-400">Erstellt</span>
<span class="text-white">{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</span> <span class="text-white">{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</span>
</div> </div>
<div class="flex justify-between">
<span class="text-gray-400">Iterationen</span>
<span class="text-white">{{ post.iterations }}</span>
</div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-400">Zeichen</span> <span class="text-gray-400">Zeichen</span>
<span class="text-white" id="charCountDisplay">{{ post.post_content | length }}</span> <span class="text-white" id="charCountDisplay">{{ post.post_content | length }}</span>
@@ -737,7 +716,7 @@ async function loadSuggestions() {
listEl.innerHTML = ''; listEl.innerHTML = '';
const header = document.createElement('p'); const header = document.createElement('p');
header.className = 'text-xs text-gray-500 mb-3'; header.className = 'text-xs text-gray-500 mb-3';
header.textContent = 'Basierend auf Kritiker-Feedback:'; header.textContent = 'Vorschläge:';
listEl.appendChild(header); listEl.appendChild(header);
suggestions.forEach(s => { suggestions.forEach(s => {

View File

@@ -10,22 +10,12 @@
onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'"> onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'">
<div class="flex items-start justify-between gap-2 mb-2"> <div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4> <h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
<span class="score-badge flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
{{ score }}
</span>
{% endif %}
</div> </div>
<div class="post-card-meta"> <div class="post-card-meta">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }} {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
</span> </span>
<span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
{{ post.iterations }}x
</span>
</div> </div>
{% if post.post_content %} {% if post.post_content %}
<p class="post-card-preview">{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}</p> <p class="post-card-preview">{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}</p>
@@ -77,10 +67,6 @@
.post-card-title { font-weight: 500; color: white; margin-bottom: 0.5rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .post-card-title { font-weight: 500; color: white; margin-bottom: 0.5rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.post-card-meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.75rem; color: #9ca3af; } .post-card-meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.75rem; color: #9ca3af; }
.post-card-preview { font-size: 0.8rem; color: #9ca3af; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid rgba(61, 72, 72, 0.6); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; } .post-card-preview { font-size: 0.8rem; color: #9ca3af; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid rgba(61, 72, 72, 0.6); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; }
.score-badge { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.125rem 0.5rem; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; }
.score-high { background: rgba(34, 197, 94, 0.2); color: #86efac; }
.score-medium { background: rgba(234, 179, 8, 0.2); color: #fde047; }
.score-low { background: rgba(239, 68, 68, 0.2); color: #fca5a5; }
.column-draft .kanban-header { border-left: 3px solid #f59e0b; } .column-draft .kanban-header { border-left: 3px solid #f59e0b; }
.column-approved .kanban-header { border-left: 3px solid #3b82f6; } .column-approved .kanban-header { border-left: 3px solid #3b82f6; }
.column-ready .kanban-header { border-left: 3px solid #22c55e; } .column-ready .kanban-header { border-left: 3px solid #22c55e; }
@@ -174,10 +160,6 @@
onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'"> onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'">
<div class="flex items-start justify-between gap-2 mb-2"> <div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4> <h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
<span class="score-badge flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">{{ score }}</span>
{% endif %}
</div> </div>
<div class="post-card-meta"> <div class="post-card-meta">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">

View File

@@ -764,10 +764,9 @@ function showFinalResult(result) {
resultHeader.innerHTML = ` resultHeader.innerHTML = `
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}"> <span class="px-3 py-1.5 rounded-lg text-sm font-medium bg-green-600/30 text-green-300">
${result.approved ? 'Approved' : 'Review needed'} Post erstellt
</span> </span>
<span class="text-gray-400">Score: <span class="text-white font-medium">${result.final_score}/100</span></span>
<span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span> <span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span>
</div> </div>
`; `;
@@ -852,44 +851,20 @@ function renderVersions(versions, feedbackList) {
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}"> ${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1} V${i + 1}
${feedbackList[i] ? `<span class="ml-1 opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button> </button>
`).join(''); `).join('');
const currentVersion = versions[currentVersionIndex]; const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
versionsContainer.innerHTML = ` versionsContainer.innerHTML = `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-6"> <div class="grid grid-cols-1 gap-6">
<div class="bg-brand-bg/50 rounded-lg p-5"> <div class="bg-brand-bg/50 rounded-lg p-5">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span> <span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? ` <span class="text-xs text-gray-500">Wird erstellt...</span>
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div> </div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre> <pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre>
</div> </div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-5 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-3">Kritik</span>
<p class="text-sm text-gray-400 mb-4">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div>
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-2 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-2">
<span class="text-yellow-500 mt-0.5">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div> </div>
`; `;
} }

View File

@@ -796,10 +796,9 @@ function showFinalResult(result) {
resultHeader.innerHTML = ` resultHeader.innerHTML = `
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}"> <span class="px-3 py-1.5 rounded-lg text-sm font-medium bg-green-600/30 text-green-300">
${result.approved ? 'Approved' : 'Review needed'} Post erstellt
</span> </span>
<span class="text-gray-400">Score: <span class="text-white font-medium">${result.final_score}/100</span></span>
<span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span> <span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span>
</div> </div>
`; `;
@@ -885,44 +884,20 @@ function renderVersions(versions, feedbackList) {
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}"> ${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1} V${i + 1}
${feedbackList[i] ? `<span class="ml-1 opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button> </button>
`).join(''); `).join('');
const currentVersion = versions[currentVersionIndex]; const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
versionsContainer.innerHTML = ` versionsContainer.innerHTML = `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-6"> <div class="grid grid-cols-1 gap-6">
<div class="bg-brand-bg/50 rounded-lg p-5"> <div class="bg-brand-bg/50 rounded-lg p-5">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span> <span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? ` <span class="text-xs text-gray-500">Wird erstellt...</span>
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div> </div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre> <pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre>
</div> </div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-5 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-3">Kritik</span>
<p class="text-sm text-gray-400 mb-4">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div>
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-2 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-2">
<span class="text-yellow-500 mt-0.5">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div> </div>
`; `;
} }

View File

@@ -807,10 +807,9 @@ function showFinalResult(result) {
resultHeader.innerHTML = ` resultHeader.innerHTML = `
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}"> <span class="px-3 py-1.5 rounded-lg text-sm font-medium bg-green-600/30 text-green-300">
${result.approved ? 'Approved' : 'Review needed'} Post erstellt
</span> </span>
<span class="text-gray-400">Score: <span class="text-white font-medium">${result.final_score}/100</span></span>
<span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span> <span class="text-gray-400">Iterationen: <span class="text-white">${result.iterations}</span></span>
</div> </div>
`; `;
@@ -898,44 +897,20 @@ function renderVersions(versions, feedbackList) {
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}"> ${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1} V${i + 1}
${feedbackList[i] ? `<span class="ml-1 opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button> </button>
`).join(''); `).join('');
const currentVersion = versions[currentVersionIndex]; const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
versionsContainer.innerHTML = ` versionsContainer.innerHTML = `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-6"> <div class="grid grid-cols-1 gap-6">
<div class="bg-brand-bg/50 rounded-lg p-5"> <div class="bg-brand-bg/50 rounded-lg p-5">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span> <span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? ` <span class="text-xs text-gray-500">Wird erstellt...</span>
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div> </div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre> <pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-80 overflow-y-auto">${currentVersion}</pre>
</div> </div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-5 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-3">Kritik</span>
<p class="text-sm text-gray-400 mb-4">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div>
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-2 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-2">
<span class="text-yellow-500 mt-0.5">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div> </div>
`; `;
} }

View File

@@ -502,38 +502,16 @@
</h1> </h1>
<div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap"> <div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap">
<span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span> <span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span>
<span class="text-gray-600">|</span>
<span>{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}</span>
</div> </div>
</div> </div>
<div class="flex items-center gap-3 flex-shrink-0"> <div class="flex items-center gap-3 flex-shrink-0">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium {{ 'bg-green-600/30 text-green-300 border border-green-600/50' if post.status == 'approved' else 'bg-yellow-600/30 text-yellow-300 border border-yellow-600/50' }}"> <span class="px-3 py-1.5 rounded-lg text-sm font-medium {{ 'bg-green-600/30 text-green-300 border border-green-600/50' if post.status == 'approved' else 'bg-yellow-600/30 text-yellow-300 border border-yellow-600/50' }}">
{{ post.status | capitalize }} {{ post.status | capitalize }}
</span> </span>
{% if final_feedback and final_feedback.get('overall_score') is not none %}
<span class="px-3 py-1.5 rounded-lg text-sm font-bold {{ 'bg-green-600/30 text-green-300' if final_feedback.get('overall_score', 0) >= 85 else 'bg-yellow-600/30 text-yellow-300' if final_feedback.get('overall_score', 0) >= 70 else 'bg-red-600/30 text-red-300' }}">
Score: {{ final_feedback.get('overall_score', 0) }}/100
</span>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- Tab Navigation -->
<div class="mb-6 border-b border-brand-bg-light">
<nav class="flex gap-1" role="tablist">
<button class="tab-btn active" onclick="switchTab('ergebnis')" role="tab" aria-selected="true" data-tab="ergebnis">
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Ergebnis
</button>
<button class="tab-btn" onclick="switchTab('iterationen')" role="tab" aria-selected="false" data-tab="iterationen">
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Iterationen
<span id="versionCount" class="ml-1 px-1.5 py-0.5 text-xs bg-brand-bg rounded">{{ post.writer_versions | length if post.writer_versions else 0 }}</span>
</button>
</nav>
</div>
<!-- Tab: Ergebnis --> <!-- Tab: Ergebnis -->
<div id="tab-ergebnis" class="tab-content active"> <div id="tab-ergebnis" class="tab-content active">
<!-- Top Section: Post + AI Suggestions --> <!-- Top Section: Post + AI Suggestions -->
@@ -778,20 +756,6 @@
</div> </div>
</div> </div>
<!-- Style Card -->
{% if style_card %}
<div class="section-card rounded-xl p-6 mb-6">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h10M7 16h6"/></svg>
Style Card
</h3>
<span class="text-xs text-gray-500">Quelle: Profilanalyse</span>
</div>
<pre class="whitespace-pre-wrap text-gray-300 text-xs bg-brand-bg/50 rounded-lg p-4 border border-brand-bg-light">{{ style_card }}</pre>
</div>
{% endif %}
<!-- Media Upload Section (Multi-Media Support) --> <!-- Media Upload Section (Multi-Media Support) -->
<div class="section-card rounded-xl p-6 mb-6"> <div class="section-card rounded-xl p-6 mb-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2"> <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
@@ -844,41 +808,8 @@
</div> </div>
</div> </div>
<!-- Bottom Section: Score, Feedback, Actions --> <!-- Bottom Section: Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- Overall Score -->
{% if final_feedback and final_feedback.get('overall_score') is not none %}
<div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-2 flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Gesamt-Score
</h3>
<div class="text-3xl font-bold text-brand-highlight">{{ final_feedback.get('overall_score', 0) }}/100</div>
</div>
{% endif %}
<!-- Final Feedback Summary -->
{% if final_feedback %}
<div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
Feedback
</h3>
<p class="text-sm text-gray-300 mb-3">{{ final_feedback.feedback }}</p>
{% if final_feedback.strengths %}
<div class="bg-green-900/10 rounded-lg p-3 border border-green-600/20">
<span class="text-xs font-medium text-green-400 block mb-2">Stärken</span>
<ul class="space-y-1">
{% for s in final_feedback.strengths %}
<li class="text-sm text-gray-400 flex items-start gap-2"><span class="text-green-400">+</span> {{ s }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="section-card rounded-xl p-6"> <div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2"> <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
@@ -930,155 +861,17 @@
</div> </div>
</div> </div>
<!-- Tab: Iterationen -->
<div id="tab-iterationen" class="tab-content">
<div class="section-card rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Iterationen
</h2>
<!-- Pagination Controls -->
<div id="paginationControls" class="flex items-center gap-2 {% if not post.writer_versions or post.writer_versions | length == 0 %}hidden{% endif %}">
<button id="prevVersion" onclick="changeVersion(-1)" class="p-2 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<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>
</button>
<span class="text-sm text-gray-400">
<span id="currentVersionNum">1</span> / <span id="totalVersionNum">{{ post.writer_versions | length if post.writer_versions else 0 }}</span>
</span>
<button id="nextVersion" onclick="changeVersion(1)" class="p-2 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<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="M9 5l7 7-7 7"/></svg>
</button>
</div>
</div>
<!-- Version Content Container -->
<div id="versionsContainer">
{% if post.writer_versions and post.writer_versions | length > 0 %}
{% for i in range(post.writer_versions | length) %}
<div class="version-panel {% if i == 0 %}block{% else %}hidden{% endif %}" data-version="{{ i }}">
<div class="flex items-center justify-between mb-3">
<span class="px-3 py-1 bg-brand-highlight/20 text-brand-highlight rounded-lg text-sm font-medium">Version {{ i + 1 }}</span>
{% if post.critic_feedback and i < post.critic_feedback | length %}
<div class="flex items-center gap-2">
{% if post.critic_feedback[i].get('overall_score') is not none %}
<span class="px-2 py-1 rounded text-xs font-medium {{ 'bg-green-600/30 text-green-300' if post.critic_feedback[i].get('overall_score', 0) >= 85 else 'bg-yellow-600/30 text-yellow-300' }}">
Score: {{ post.critic_feedback[i].get('overall_score', 0) }}/100
</span>
{% endif %}
{% if post.critic_feedback[i].get('approved') %}
<span class="px-2 py-1 bg-green-600/30 text-green-300 rounded text-xs">Approved</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="bg-brand-bg/30 rounded-lg p-4 mb-4">
<pre class="whitespace-pre-wrap text-gray-300 font-sans text-sm">{{ post.writer_versions[i] }}</pre>
</div>
{% if post.critic_feedback and i < post.critic_feedback | length %}
{% set fb = post.critic_feedback[i] %}
<div class="bg-brand-bg/20 rounded-lg p-4 border border-brand-bg-light">
<h4 class="text-sm font-medium text-white mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
Critic Feedback
</h4>
<p class="text-sm text-gray-400 mb-3">{{ fb.feedback }}</p>
<div class="grid md:grid-cols-2 gap-4">
{% if fb.strengths %}
<div class="bg-green-900/10 rounded-lg p-3 border border-green-600/20">
<span class="text-xs font-medium text-green-400 block mb-2">Stärken</span>
<ul class="space-y-1 text-sm text-gray-400">
{% for s in fb.strengths %}
<li class="flex items-start gap-2"><span class="text-green-400 mt-0.5">+</span> {{ s }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if fb.improvements %}
<div class="bg-yellow-900/10 rounded-lg p-3 border border-yellow-600/20">
<span class="text-xs font-medium text-yellow-400 block mb-2">Verbesserungen</span>
<ul class="space-y-1 text-sm text-gray-400">
{% for imp in fb.improvements %}
<li class="flex items-start gap-2"><span class="text-yellow-400 mt-0.5">-</span> {{ imp }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<p id="noVersionsMsg" class="text-sm text-gray-400 text-center py-4">Noch keine Versionen vorhanden.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
let currentVersion = 0;
let totalVersions = {{ post.writer_versions | length if post.writer_versions else 0 }};
let currentView = 'preview'; let currentView = 'preview';
let contentExpanded = false; let contentExpanded = false;
let originalContent = ''; let originalContent = '';
const postId = '{{ post.id }}'; const postId = '{{ post.id }}';
// Function to add a new version to the UI
function addNewVersion(content) { function addNewVersion(content) {
totalVersions++; // Versions are no longer shown in the UI.
// Update version counts in UI
document.getElementById('versionCount').textContent = totalVersions;
document.getElementById('totalVersionNum').textContent = totalVersions;
// Show pagination controls if this is the first version
const paginationControls = document.getElementById('paginationControls');
if (paginationControls) {
paginationControls.classList.remove('hidden');
}
// Remove "no versions" message if present
const noVersionsMsg = document.getElementById('noVersionsMsg');
if (noVersionsMsg) {
noVersionsMsg.remove();
}
// Hide all existing version panels
document.querySelectorAll('.version-panel').forEach(panel => {
panel.classList.add('hidden');
panel.classList.remove('block');
});
// Create new version panel
const container = document.getElementById('versionsContainer');
const newPanel = document.createElement('div');
newPanel.className = 'version-panel block';
newPanel.dataset.version = totalVersions - 1;
newPanel.innerHTML = `
<div class="flex items-center justify-between mb-3">
<span class="px-3 py-1 bg-brand-highlight/20 text-brand-highlight rounded-lg text-sm font-medium">Version ${totalVersions}</span>
<span class="px-2 py-1 bg-blue-600/30 text-blue-300 rounded text-xs">Manuell bearbeitet</span>
</div>
<div class="bg-brand-bg/30 rounded-lg p-4 mb-4">
<pre class="whitespace-pre-wrap text-gray-300 font-sans text-sm">${escapeHtml(content)}</pre>
</div>
`;
container.appendChild(newPanel);
// Update current version to the new one
currentVersion = totalVersions - 1;
document.getElementById('currentVersionNum').textContent = totalVersions;
// Update button states
const prevBtn = document.getElementById('prevVersion');
const nextBtn = document.getElementById('nextVersion');
if (prevBtn) prevBtn.disabled = currentVersion === 0;
if (nextBtn) nextBtn.disabled = true;
} }
// View Toggle functions // View Toggle functions
@@ -1266,7 +1059,7 @@ async function loadSuggestions() {
// Add header // Add header
const header = document.createElement('p'); const header = document.createElement('p');
header.className = 'text-xs text-gray-500 mb-3'; header.className = 'text-xs text-gray-500 mb-3';
header.textContent = 'Basierend auf Kritiker-Feedback:'; header.textContent = 'Vorschläge:';
listEl.appendChild(header); listEl.appendChild(header);
suggestions.forEach((s, i) => { suggestions.forEach((s, i) => {
@@ -1549,27 +1342,6 @@ function copyToClipboard() {
}); });
} }
function changeVersion(delta) {
const newVersion = currentVersion + delta;
if (newVersion < 0 || newVersion >= totalVersions) return;
// Hide current
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.add('hidden');
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.remove('block');
// Show new
currentVersion = newVersion;
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.remove('hidden');
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.add('block');
// Update counter
document.getElementById('currentVersionNum').textContent = currentVersion + 1;
// Update button states
document.getElementById('prevVersion').disabled = currentVersion === 0;
document.getElementById('nextVersion').disabled = currentVersion === totalVersions - 1;
}
// ==================== MULTI-MEDIA UPLOAD ==================== // ==================== MULTI-MEDIA UPLOAD ====================
let currentMediaItems = {{ media_items_dict | tojson | safe }}; let currentMediaItems = {{ media_items_dict | tojson | safe }};
@@ -1999,12 +1771,6 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize original content from DOM // Initialize original content from DOM
originalContent = document.getElementById('finalPostContent').textContent; originalContent = document.getElementById('finalPostContent').textContent;
if (totalVersions > 0) {
const prevBtn = document.getElementById('prevVersion');
const nextBtn = document.getElementById('nextVersion');
if (prevBtn) prevBtn.disabled = true;
if (nextBtn) nextBtn.disabled = totalVersions <= 1;
}
// Initialize LinkedIn preview // Initialize LinkedIn preview
initLinkedInPreview(); initLinkedInPreview();

View File

@@ -10,22 +10,12 @@
onclick="window.location.href='/posts/{{ post.id }}'"> onclick="window.location.href='/posts/{{ post.id }}'">
<div class="flex items-start justify-between gap-2 mb-2"> <div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4> <h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
<span class="score-badge flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
{{ score }}
</span>
{% endif %}
</div> </div>
<div class="post-card-meta"> <div class="post-card-meta">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }} {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
</span> </span>
<span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
{{ post.iterations }}x
</span>
</div> </div>
{% if post.post_content %} {% if post.post_content %}
<p class="post-card-preview">{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}</p> <p class="post-card-preview">{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}</p>
@@ -132,18 +122,6 @@
overflow: hidden; overflow: hidden;
line-height: 1.4; line-height: 1.4;
} }
.score-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
}
.score-high { background: rgba(34, 197, 94, 0.2); color: #86efac; }
.score-medium { background: rgba(234, 179, 8, 0.2); color: #fde047; }
.score-low { background: rgba(239, 68, 68, 0.2); color: #fca5a5; }
.column-draft .kanban-header { border-left: 3px solid #f59e0b; } .column-draft .kanban-header { border-left: 3px solid #f59e0b; }
.column-approved .kanban-header { border-left: 3px solid #3b82f6; } .column-approved .kanban-header { border-left: 3px solid #3b82f6; }
.column-ready .kanban-header { border-left: 3px solid #22c55e; } .column-ready .kanban-header { border-left: 3px solid #22c55e; }

View File

@@ -74,13 +74,6 @@
{% else %} {% else %}
<div>erstellt {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</div> <div>erstellt {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</div>
{% endif %} {% endif %}
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
<div class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-semibold
{{ 'bg-green-900/50 text-green-300' if score >= 85 else 'bg-yellow-900/50 text-yellow-300' if score >= 70 else 'bg-red-900/50 text-red-300' }}">
{{ score }}
</div>
{% endif %}
</div> </div>
</div> </div>
</a> </a>

View File

@@ -2397,9 +2397,8 @@ async def create_post(
try: try:
def progress_callback(message: str, iteration: int, max_iterations: int, score: int = None, versions: list = None, feedback_list: list = None): def progress_callback(message: str, iteration: int, max_iterations: int, score: int = None, versions: list = None, feedback_list: list = None):
progress = int((iteration / max_iterations) * 100) if iteration > 0 else 5 progress = int((iteration / max_iterations) * 100) if iteration > 0 else 5
score_text = f" (Score: {score}/100)" if score else ""
progress_store[task_id] = { progress_store[task_id] = {
"status": "running", "message": f"{message}{score_text}", "progress": progress, "status": "running", "message": message, "progress": progress,
"iteration": iteration, "max_iterations": max_iterations, "iteration": iteration, "max_iterations": max_iterations,
"versions": versions or [], "feedback_list": feedback_list or [] "versions": versions or [], "feedback_list": feedback_list or []
} }