Chat assistant

This commit is contained in:
2026-02-15 17:24:48 +01:00
parent 31150000fd
commit f772659201
7 changed files with 740 additions and 19 deletions

View File

@@ -905,21 +905,21 @@ Strategy Weight: {strategy_weight:.1f} / 1.0
improvements_text += f"- {imp}\n"
# Revision mode with structured feedback
return f"""ÜBERARBEITE den Post basierend auf dem Kritiker-Feedback.
score_text = f"**AKTUELLER SCORE:** {critic_result.get('overall_score', 'N/A')}/100\n\n" if critic_result else ""
return f"""ÜBERARBEITE den Post basierend auf dem Feedback.
**VORHERIGE VERSION:**
{previous_version}
**AKTUELLER SCORE:** {critic_result.get('overall_score', 'N/A')}/100
**FEEDBACK:**
{score_text}**FEEDBACK:**
{feedback}
{specific_changes_text}
{improvements_text}
**DEINE AUFGABE:**
1. Führe die konkreten Änderungen EXAKT durch
2. Behalte alles bei was GUT bewertet wurde
3. Der überarbeitete Post soll mindestens 85 Punkte erreichen
1. Führe die Änderungen durch wie im Feedback beschrieben
2. Behalte alles bei was gut funktioniert
3. Der überarbeitete Post soll die Anforderungen erfüllen
Gib NUR den überarbeiteten Post zurück - keine Kommentare."""

View File

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

View File

@@ -0,0 +1,414 @@
{% extends "base.html" %}
{% block title %}Chat Assistent{% endblock %}
{% block content %}
<style>
/* Make chat page full height and remove padding */
#mainContent > div {
padding: 0 !important;
}
/* Fixed elements adjust to sidebar with smooth transition */
.chat-fixed-header,
.chat-fixed-input {
left: 256px;
right: 0;
transition: left 0.3s ease;
}
/* When sidebar is collapsed, adjust fixed elements */
aside.collapsed ~ main .chat-fixed-header,
aside.collapsed ~ main .chat-fixed-input {
left: 64px;
}
/* Bouncing dots animation */
.dot {
animation: bounce 1.4s infinite ease-in-out;
display: inline-block;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
</style>
<!-- Header (Fixed at top) -->
<div class="chat-fixed-header fixed top-0 bg-brand-bg z-20">
<div class="px-8 py-4">
<h1 class="text-xl font-bold text-white">💬 Chat Assistent</h1>
</div>
</div>
<!-- Messages Area (Full page scroll with padding for fixed header and input) -->
<div id="chat-messages" class="px-8 space-y-4 pb-64 max-w-[80%] mx-auto" style="padding-top: 80px;">
<!-- Welcome Message -->
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-lg">🤖</span>
</div>
<div class="max-w-[70%]">
<div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600">
<p class="text-gray-300">
Hallo! Ich helfe dir beim Erstellen deines LinkedIn-Posts.
Beschreibe mir einfach, worüber du schreiben möchtest, und ich erstelle einen ersten Entwurf für dich.
</p>
<p class="text-gray-400 text-sm mt-2">
Du kannst mich danach bitten, den Post anzupassen, umzuschreiben oder zu verbessern.
</p>
</div>
</div>
</div>
</div>
<!-- Input Area (Fixed at bottom, floating over messages) -->
<div class="chat-fixed-input fixed bottom-0 bg-brand-bg z-10">
<div class="pt-4 pb-6 px-8">
<div class="space-y-3 mx-auto" style="max-width: 768px;">
<!-- Post Type Selector (Chips) - Above Input -->
<div class="flex flex-wrap gap-1.5" id="post-type-chips">
{% for pt in post_types %}
<button
onclick="selectPostType('{{ pt.id }}', this)"
data-post-type-id="{{ pt.id }}"
class="post-type-chip px-3 py-1 rounded-full text-xs font-medium transition-all
{% if loop.first %}bg-brand-highlight text-black{% else %}bg-brand-bg-dark text-gray-400 hover:bg-brand-bg-light hover:text-gray-300{% endif %}"
{% if loop.first %}data-selected="true"{% endif %}>
{{ pt.name }}
</button>
{% endfor %}
</div>
<!-- Input Bar -->
<div class="flex gap-3 items-center">
<div class="flex-1 relative">
<input
type="text"
id="chat-input"
placeholder="Beschreibe deinen Post..."
class="w-full input-bg border border-gray-600 rounded-full px-6 py-3 text-white focus:outline-none focus:border-brand-highlight transition-colors"
onkeydown="handleChatKeydown(event)">
</div>
<button
onclick="sendMessage()"
id="send-btn"
class="w-12 h-12 bg-brand-highlight hover:bg-yellow-500 text-black rounded-full transition-all flex items-center justify-center flex-shrink-0 hover:scale-110"
title="Senden (Enter)">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
</button>
<button
onclick="savePost()"
id="save-btn"
class="hidden w-12 h-12 bg-green-600 hover:bg-green-500 text-white rounded-full transition-all items-center justify-center flex-shrink-0 hover:scale-110"
title="Post speichern">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<script>
let chatHistory = [];
let currentPost = null;
let conversationId = null;
let selectedPostTypeId = null;
// User profile data
const userProfilePicture = "{{ profile_picture or '' }}";
const userInitial = "{{ (session.display_name or session.linkedin_name or 'U')[0] | upper }}";
// Initialize: Select first post type
document.addEventListener('DOMContentLoaded', () => {
const firstChip = document.querySelector('.post-type-chip[data-selected="true"]');
if (firstChip) {
selectedPostTypeId = firstChip.dataset.postTypeId;
}
});
function selectPostType(postTypeId, element) {
// Update selected state
selectedPostTypeId = postTypeId;
// Update chip styles
document.querySelectorAll('.post-type-chip').forEach(chip => {
chip.classList.remove('bg-brand-highlight', 'text-black');
chip.classList.add('bg-brand-bg-dark', 'text-gray-400');
chip.removeAttribute('data-selected');
});
element.classList.remove('bg-brand-bg-dark', 'text-gray-400');
element.classList.add('bg-brand-highlight', 'text-black');
element.setAttribute('data-selected', 'true');
}
function handleChatKeydown(event) {
// Send on Enter (not Shift+Enter)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
showToast('Bitte gib eine Nachricht ein', 'error');
return;
}
if (!selectedPostTypeId) {
showToast('Bitte wähle einen Post-Typ aus', 'error');
return;
}
// Add user message to chat
addMessageToChat('user', message);
input.value = '';
// Add temporary "generating" message
const tempMessageId = 'temp-generating-msg';
const container = document.getElementById('chat-messages');
const tempMsg = document.createElement('div');
tempMsg.id = tempMessageId;
tempMsg.className = 'flex items-start gap-3';
tempMsg.innerHTML = `
<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-lg">🤖</span>
</div>
<div class="max-w-[70%]">
<div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600">
<p class="text-gray-300">
wird generiert<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
</p>
</div>
</div>
`;
container.appendChild(tempMsg);
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
// Disable input while processing - show black spinner in button
const sendBtn = document.getElementById('send-btn');
const originalHTML = sendBtn.innerHTML;
sendBtn.disabled = true;
sendBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" opacity="0.3"/><path d="M12 2v4c3.31 0 6 2.69 6 6h4c0-5.52-4.48-10-10-10z"/></svg>';
try {
const endpoint = currentPost ? '/api/employee/chat/refine' : '/api/employee/chat/generate';
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
post_type_id: selectedPostTypeId,
conversation_id: conversationId,
current_post: currentPost,
chat_history: chatHistory
})
});
const result = await response.json();
// Remove temporary "generating" message
const tempMsg = document.getElementById(tempMessageId);
if (tempMsg) tempMsg.remove();
if (result.success) {
currentPost = result.post;
conversationId = result.conversation_id;
// Add AI response to chat
addMessageToChat('ai', result.explanation || 'Hier ist dein Post:', result.post);
// Show save button
const saveBtn = document.getElementById('save-btn');
saveBtn.classList.remove('hidden');
saveBtn.classList.add('flex');
// Store in history
chatHistory.push({
user: message,
ai: result.post,
explanation: result.explanation
});
} else {
showToast('Fehler: ' + (result.error || 'Unbekannter Fehler'), 'error');
addMessageToChat('ai', '❌ Entschuldigung, es gab einen Fehler. Bitte versuche es erneut.');
}
} catch (error) {
console.error('Error:', error);
showToast('Netzwerkfehler beim Generieren', 'error');
// Remove temporary message on error
const tempMsg = document.getElementById(tempMessageId);
if (tempMsg) tempMsg.remove();
addMessageToChat('ai', '❌ Netzwerkfehler. Bitte überprüfe deine Verbindung.');
} finally {
sendBtn.disabled = false;
sendBtn.innerHTML = originalHTML;
}
}
function addMessageToChat(type, text, post = null) {
const container = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
if (type === 'user') {
// User messages: right-aligned, avatar on right, max-width 70%
messageDiv.className = 'flex items-start gap-3 justify-end';
const userAvatarHtml = userProfilePicture
? `<img src="${userProfilePicture}" alt="User" class="w-full h-full object-cover" referrerpolicy="no-referrer">`
: `<span class="text-brand-bg-dark font-bold text-sm">${userInitial}</span>`;
messageDiv.innerHTML = `
<div class="max-w-[70%]">
<div class="bg-blue-900/30 rounded-2xl p-4 border border-blue-700/50">
<p class="text-white">${escapeHtml(text)}</p>
</div>
</div>
<div class="w-8 h-8 bg-brand-highlight rounded-full overflow-hidden flex items-center justify-center flex-shrink-0">
${userAvatarHtml}
</div>
`;
} else {
// Bot messages: left-aligned, avatar on left, max-width 70%
messageDiv.className = 'flex items-start gap-3';
const postHtml = post ? `
<div class="mt-3 p-4 bg-brand-bg rounded-2xl border border-brand-highlight/30">
<div class="flex items-center gap-2 mb-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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="text-brand-highlight font-medium text-sm">Post-Entwurf</span>
</div>
<div class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">${escapeHtml(post)}</div>
</div>
` : '';
messageDiv.innerHTML = `
<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-lg">🤖</span>
</div>
<div class="max-w-[70%]">
<div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600">
<p class="text-gray-300">${escapeHtml(text)}</p>
${postHtml}
</div>
</div>
`;
}
container.appendChild(messageDiv);
// Scroll to bottom smoothly
setTimeout(() => {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}, 100);
}
async function savePost() {
if (!currentPost) {
showToast('Kein Post zum Speichern vorhanden', 'error');
return;
}
if (!selectedPostTypeId) {
showToast('Bitte wähle einen Post-Typ aus', 'error');
return;
}
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = true;
const originalHTML = saveBtn.innerHTML;
saveBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" 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>';
try {
const response = await fetch('/api/employee/chat/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
post_content: currentPost,
post_type_id: selectedPostTypeId,
conversation_id: conversationId,
chat_history: chatHistory
})
});
const result = await response.json();
if (result.success) {
showToast('Post erfolgreich gespeichert!', 'success');
// Redirect to post details after short delay
setTimeout(() => {
window.location.href = `/posts/${result.post_id}`;
}, 1000);
} else {
showToast('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler'), 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
}
} catch (error) {
console.error('Error:', error);
showToast('Netzwerkfehler beim Speichern', 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
const colors = {
success: 'bg-green-600 border-green-500',
error: 'bg-red-600 border-red-500',
info: 'bg-blue-600 border-blue-500',
warning: 'bg-yellow-600 border-yellow-500'
};
const icons = {
success: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
error: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
info: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
warning: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>'
};
toast.className = `fixed bottom-8 right-8 ${colors[type]} text-white px-6 py-4 rounded-xl shadow-2xl border-2 z-50 flex items-center gap-3 transform transition-all duration-300`;
toast.innerHTML = `
${icons[type]}
<span class="font-medium">${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(1rem)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
</script>
{% endblock %}

View File

@@ -10,8 +10,8 @@
onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'">
<div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
{% set score = post.critic_feedback[-1].overall_score %}
{% 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>

View File

@@ -510,9 +510,9 @@
<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 }}
</span>
{% if final_feedback %}
<span class="px-3 py-1.5 rounded-lg text-sm font-bold {{ 'bg-green-600/30 text-green-300' if final_feedback.overall_score >= 85 else 'bg-yellow-600/30 text-yellow-300' if final_feedback.overall_score >= 70 else 'bg-red-600/30 text-red-300' }}">
Score: {{ final_feedback.overall_score }}/100
{% 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>
@@ -833,11 +833,11 @@
<!-- Bottom Section: Score, Feedback, Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- Score Breakdown -->
{% if final_feedback and final_feedback.scores %}
{% if final_feedback and final_feedback.get('scores') and final_feedback.get('overall_score') is not none %}
<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="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>
Score: {{ final_feedback.overall_score }}/100
Score: {{ final_feedback.get('overall_score', 0) }}/100
</h3>
<div class="space-y-3">
<div>
@@ -943,10 +943,12 @@
<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">
<span class="px-2 py-1 rounded text-xs font-medium {{ 'bg-green-600/30 text-green-300' if post.critic_feedback[i].overall_score >= 85 else 'bg-yellow-600/30 text-yellow-300' }}">
Score: {{ post.critic_feedback[i].overall_score }}/100
{% 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>
{% if post.critic_feedback[i].approved %}
{% 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>

View File

@@ -10,8 +10,8 @@
onclick="window.location.href='/posts/{{ post.id }}'">
<div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
{% set score = post.critic_feedback[-1].overall_score %}
{% 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>

View File

@@ -1354,7 +1354,9 @@ async def posts_page(request: Request):
"profile_picture": profile_picture
})
except Exception as e:
import traceback
logger.error(f"Error loading posts: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return templates.TemplateResponse("posts.html", {
"request": request,
"page": "posts",
@@ -1628,7 +1630,9 @@ async def post_detail_page(request: Request, post_id: str):
"media_items_dict": media_items_dict
})
except Exception as e:
import traceback
logger.error(f"Error loading post detail: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return RedirectResponse(url="/posts", status_code=302)
@@ -1690,6 +1694,35 @@ async def create_post_page(request: Request):
})
@user_router.get("/chat-create", response_class=HTMLResponse)
async def chat_create_page(request: Request):
"""Chat-based post creation page."""
session = require_user_session(request)
if not session:
return RedirectResponse(url="/login", status_code=302)
user_id = UUID(session.user_id)
# Get post types
post_types = await db.get_post_types(user_id)
if not post_types:
return templates.TemplateResponse("error.html", {
"request": request,
"session": session,
"error": "Keine Post-Typen gefunden. Bitte erstelle zuerst Post-Typen."
})
profile_picture = await get_user_avatar(session, user_id)
return templates.TemplateResponse("chat_create.html", {
"request": request,
"page": "chat-create",
"session": session,
"post_types": post_types,
"profile_picture": profile_picture
})
@user_router.get("/status", response_class=HTMLResponse)
async def status_page(request: Request):
"""User's status page."""
@@ -3252,6 +3285,271 @@ async def save_all_and_reanalyze(request: Request, background_tasks: BackgroundT
return JSONResponse({"error": str(e)}, status_code=500)
@user_router.post("/api/employee/chat/generate")
async def chat_generate_post(request: Request):
"""Generate initial post from chat message."""
session = require_user_session(request)
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
data = await request.json()
message = data.get("message", "").strip()
post_type_id = data.get("post_type_id")
if not message:
return JSONResponse({"success": False, "error": "Nachricht erforderlich"})
if not post_type_id:
return JSONResponse({"success": False, "error": "Post-Typ erforderlich"})
user_id = UUID(session.user_id)
# Get post type info
post_type = await db.get_post_type(UUID(post_type_id))
if not post_type:
return JSONResponse({"success": False, "error": "Post-Typ nicht gefunden"})
# Get profile analysis
profile_analysis = await db.get_profile_analysis(user_id)
if not profile_analysis:
return JSONResponse({"success": False, "error": "Profil-Analyse nicht gefunden"})
# Get company strategy if available
company_strategy = None
profile = await db.get_profile(user_id)
if profile and profile.company_id:
company = await db.get_company(profile.company_id)
if company and company.company_strategy:
company_strategy = company.company_strategy
# Get example posts for style reference
linkedin_posts = await db.get_posts_by_type(user_id, UUID(post_type_id))
if len(linkedin_posts) < 3:
linkedin_posts = await db.get_linkedin_posts(user_id)
example_post_texts = [
post.post_text for post in linkedin_posts
if post.post_text and len(post.post_text) > 100
][:10]
# Generate post using writer agent with user's content as primary focus
from src.agents.writer import WriterAgent
writer = WriterAgent()
# Create a topic structure from user's message
topic = {
"title": message[:100],
"fact": message,
"relevance": "User-specified content"
}
# Generate post
post_content = await writer.process(
topic=topic,
profile_analysis=profile_analysis.full_analysis,
example_posts=example_post_texts,
post_type=post_type,
user_thoughts=message, # CRITICAL: User's input as primary content
company_strategy=company_strategy,
strategy_weight=post_type.strategy_weight
)
# Generate conversation ID
import uuid
conversation_id = str(uuid.uuid4())
return JSONResponse({
"success": True,
"post": post_content,
"conversation_id": conversation_id,
"explanation": "Hier ist dein erster Entwurf basierend auf deiner Beschreibung:"
})
except Exception as e:
logger.error(f"Error generating chat post: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@user_router.post("/api/employee/chat/refine")
async def chat_refine_post(request: Request):
"""Refine existing post based on user feedback."""
session = require_user_session(request)
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
data = await request.json()
message = data.get("message", "").strip()
current_post = data.get("current_post", "")
post_type_id = data.get("post_type_id")
chat_history = data.get("chat_history", [])
if not message:
return JSONResponse({"success": False, "error": "Nachricht erforderlich"})
if not current_post:
return JSONResponse({"success": False, "error": "Kein Post zum Verfeinern vorhanden"})
if not post_type_id:
return JSONResponse({"success": False, "error": "Post-Typ erforderlich"})
user_id = UUID(session.user_id)
# Get post type info
post_type = await db.get_post_type(UUID(post_type_id))
if not post_type:
return JSONResponse({"success": False, "error": "Post-Typ nicht gefunden"})
# Get profile analysis
profile_analysis = await db.get_profile_analysis(user_id)
if not profile_analysis:
return JSONResponse({"success": False, "error": "Profil-Analyse nicht gefunden"})
# Ensure full_analysis is a dict
full_analysis = profile_analysis.full_analysis if profile_analysis.full_analysis else {}
if not isinstance(full_analysis, dict):
logger.warning(f"full_analysis is not a dict: {type(full_analysis)}")
full_analysis = {}
# Get company strategy if available
company_strategy = None
profile = await db.get_profile(user_id)
if profile and profile.company_id:
company = await db.get_company(profile.company_id)
if company and company.company_strategy:
company_strategy = company.company_strategy
# Ensure it's a dict
if not isinstance(company_strategy, dict):
logger.warning(f"company_strategy is not a dict: {type(company_strategy)}")
company_strategy = None
# Get example posts
linkedin_posts = await db.get_posts_by_type(user_id, UUID(post_type_id))
if len(linkedin_posts) < 3:
linkedin_posts = await db.get_linkedin_posts(user_id)
example_post_texts = [
post.post_text for post in linkedin_posts
if post.post_text and len(post.post_text) > 100
][:10]
# Refine post using writer with feedback
from src.agents.writer import WriterAgent
writer = WriterAgent()
topic = {
"title": "Chat refinement",
"fact": message,
"relevance": "User refinement request"
}
# Use writer's revision capability
refined_post = await writer.process(
topic=topic,
profile_analysis=full_analysis,
example_posts=example_post_texts,
feedback=message, # User's refinement instruction
previous_version=current_post,
post_type=post_type,
user_thoughts=message,
company_strategy=company_strategy,
strategy_weight=getattr(post_type, 'strategy_weight', 0.5)
)
return JSONResponse({
"success": True,
"post": refined_post,
"conversation_id": data.get("conversation_id"),
"explanation": "Ich habe den Post angepasst:"
})
except Exception as e:
import traceback
logger.error(f"Error refining chat post: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@user_router.post("/api/employee/chat/save")
async def chat_save_post(request: Request):
"""Save chat-generated post to database."""
session = require_user_session(request)
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
data = await request.json()
post_content = data.get("post_content", "").strip()
post_type_id = data.get("post_type_id")
chat_history = data.get("chat_history", [])
if not post_content:
return JSONResponse({"success": False, "error": "Post-Inhalt erforderlich"})
if not post_type_id:
return JSONResponse({"success": False, "error": "Post-Typ erforderlich"})
user_id = UUID(session.user_id)
# Extract title from first sentence of post
first_sentence = post_content.split('\n')[0].strip()
if len(first_sentence) > 100:
title = first_sentence[:97] + "..."
else:
title = first_sentence if first_sentence else "Chat-generierter Post"
# Create GeneratedPost with status draft
from src.database.models import GeneratedPost
import uuid as uuid_lib
post_id = uuid_lib.uuid4()
# Extract all AI-generated versions from chat history
writer_versions = []
critic_feedback_list = []
for item in chat_history:
if 'ai' in item and item['ai']:
writer_versions.append(item['ai'])
# Store user feedback as "critic feedback"
if 'user' in item and item['user']:
critic_feedback_list.append({
'feedback': item['user'],
'explanation': item.get('explanation', '')
})
# Add final version
writer_versions.append(post_content)
num_iterations = len(writer_versions)
generated_post = GeneratedPost(
id=post_id,
user_id=user_id,
post_content=post_content,
post_type_id=UUID(post_type_id),
status="draft",
iterations=num_iterations,
writer_versions=writer_versions, # All iterations saved here
critic_feedback=critic_feedback_list, # User feedback saved here
topic_title=title,
created_at=datetime.now(timezone.utc)
)
saved_post = await db.save_generated_post(generated_post)
return JSONResponse({
"success": True,
"post_id": str(saved_post.id),
"message": "Post erfolgreich gespeichert"
})
except Exception as e:
logger.error(f"Error saving chat post: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@user_router.post("/api/company/invite")
async def send_company_invitation(request: Request):
"""Send invitation to a new employee."""