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

@@ -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."""