simplified ui and improved hook generation and research
This commit is contained in:
@@ -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."""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user