Chat optimizations

This commit is contained in:
2026-02-16 16:59:01 +01:00
parent f772659201
commit a062383af0
3 changed files with 589 additions and 33 deletions

View File

@@ -86,15 +86,15 @@ async def fix_all_posts(apply: bool = False):
Args:
apply: If True, apply changes to database. If False, just preview.
"""
logger.info("Loading all customers...")
customers = await db.list_customers()
logger.info("Loading all users...")
users = await db.list_users()
total_posts = 0
posts_with_markdown = 0
fixed_posts = []
for customer in customers:
posts = await db.get_generated_posts(customer.id)
for user in users:
posts = await db.get_generated_posts(user.id)
for post in posts:
total_posts += 1
@@ -107,9 +107,12 @@ async def fix_all_posts(apply: bool = False):
original = post.post_content
converted = convert_markdown_bold(original)
# Get user display name (email or linkedin name)
user_name = user.email or user.linkedin_name or str(user.id)
fixed_posts.append({
'id': post.id,
'customer': customer.name,
'user': user_name,
'topic': post.topic_title,
'original': original,
'converted': converted,
@@ -118,7 +121,7 @@ async def fix_all_posts(apply: bool = False):
# Show preview
print(f"\n{'='*60}")
print(f"Post: {post.topic_title}")
print(f"Customer: {customer.name}")
print(f"User: {user_name}")
print(f"ID: {post.id}")
print(f"{'-'*60}")

View File

@@ -3,6 +3,32 @@
{% block title %}Chat Assistent{% endblock %}
{% block content %}
<!-- Posts Sidebar -->
<div class="posts-sidebar">
<div class="posts-sidebar-header">
<button onclick="startNewPost()">
+ Neuer Post
</button>
</div>
<div class="posts-sidebar-list">
{% if saved_posts %}
{% for post in saved_posts %}
<div class="post-sidebar-item"
data-post-id="{{ post.id }}"
onclick="loadPostHistory('{{ post.id }}')">
<div class="post-sidebar-title">{{ post.topic_title }}</div>
<span class="post-sidebar-status status-{{ post.status }}">{{ post.status }}</span>
</div>
{% endfor %}
{% else %}
<div class="posts-sidebar-empty">
Noch keine Posts vorhanden
</div>
{% endif %}
</div>
</div>
<style>
/* Make chat page full height and remove padding */
#mainContent > div {
@@ -35,6 +61,168 @@ aside.collapsed ~ main .chat-fixed-input {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
/* Posts Sidebar */
.posts-sidebar {
position: fixed;
left: 256px;
top: 0;
width: 280px;
height: 100vh;
background-color: #2d3838;
border-right: 1px solid #5a6868;
z-index: 15;
display: flex;
flex-direction: column;
transition: left 0.3s ease;
}
/* When main sidebar is collapsed */
aside.collapsed ~ main .posts-sidebar {
left: 64px;
}
/* Hide on smaller screens */
@media (max-width: 1280px) {
.posts-sidebar {
display: none !important;
}
#chat-messages {
margin-left: 0 !important;
}
.chat-fixed-header,
.chat-fixed-input {
left: 256px !important;
}
aside.collapsed ~ main .chat-fixed-header,
aside.collapsed ~ main .chat-fixed-input {
left: 64px !important;
}
}
/* Sidebar Header */
.posts-sidebar-header {
padding: 1rem;
border-bottom: 1px solid #5a6868;
}
.posts-sidebar-header button {
width: 100%;
padding: 0.625rem 1rem;
background-color: #ffc700;
color: #2d3838;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
transition: background-color 0.2s;
border: none;
cursor: pointer;
}
.posts-sidebar-header button:hover {
background-color: #e6b300;
}
/* Posts List */
.posts-sidebar-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* Post Item */
.post-sidebar-item {
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
background-color: #3d4848;
border-left: 3px solid transparent;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.post-sidebar-item:hover {
background-color: #4a5858;
border-left-color: #ffc700;
}
.post-sidebar-item.active {
background-color: #4a5858;
border-left-color: #ffc700;
}
.post-sidebar-title {
font-size: 0.875rem;
color: #e5e5e5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.post-sidebar-status {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
flex-shrink: 0;
}
.status-draft {
background-color: rgba(107, 114, 128, 0.3);
color: #d1d5db;
}
.status-approved {
background-color: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
.status-ready {
background-color: rgba(59, 130, 246, 0.2);
color: #93c5fd;
}
.status-scheduled {
background-color: rgba(251, 146, 60, 0.2);
color: #fdba74;
}
.status-published {
background-color: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
.status-rejected {
background-color: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
/* Empty state */
.posts-sidebar-empty {
padding: 2rem 1rem;
text-align: center;
color: #9ca3af;
font-size: 0.875rem;
}
/* Adjust chat area for sidebar */
#chat-messages {
margin-left: 280px;
}
.chat-fixed-header,
.chat-fixed-input {
left: 536px; /* 256px (main sidebar) + 280px (posts sidebar) */
}
aside.collapsed ~ main .chat-fixed-header,
aside.collapsed ~ main .chat-fixed-input {
left: 344px; /* 64px (collapsed main sidebar) + 280px (posts sidebar) */
}
</style>
<!-- Header (Fixed at top) -->
@@ -121,6 +309,7 @@ let chatHistory = [];
let currentPost = null;
let conversationId = null;
let selectedPostTypeId = null;
let currentLoadedPostId = null; // Track active post
// User profile data
const userProfilePicture = "{{ profile_picture or '' }}";
@@ -231,17 +420,22 @@ async function sendMessage() {
// 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
});
// Auto-save if this is a loaded post, otherwise show save button
if (currentLoadedPostId) {
await autoSaveLoadedPost();
} else {
// Show save button only for new posts
const saveBtn = document.getElementById('save-btn');
saveBtn.classList.remove('hidden');
saveBtn.classList.add('flex');
}
} else {
showToast('Fehler: ' + (result.error || 'Unbekannter Fehler'), 'error');
addMessageToChat('ai', '❌ Entschuldigung, es gab einen Fehler. Bitte versuche es erneut.');
@@ -340,30 +534,55 @@ async function savePost() {
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
})
});
// If this is a loaded post, update it instead of creating a new one
if (currentLoadedPostId) {
const response = await fetch(`/api/employee/chat/update/${currentLoadedPostId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
post_content: currentPost,
chat_history: chatHistory
})
});
const result = await response.json();
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);
if (result.success) {
showToast('Post erfolgreich aktualisiert!', 'success');
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
} else {
showToast('Fehler beim Aktualisieren: ' + (result.error || 'Unbekannter Fehler'), 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
}
} else {
showToast('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler'), 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
// Create new post
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);
@@ -379,6 +598,202 @@ function escapeHtml(text) {
return div.innerHTML;
}
function startNewPost() {
// Reset state
chatHistory = [];
currentPost = null;
conversationId = null;
currentLoadedPostId = null;
// Remove active class from all sidebar items
document.querySelectorAll('.post-sidebar-item').forEach(item => {
item.classList.remove('active');
});
// Clear chat area (keep only welcome message)
const container = document.getElementById('chat-messages');
container.innerHTML = `
<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>
`;
// Hide save button
const saveBtn = document.getElementById('save-btn');
saveBtn.classList.add('hidden');
saveBtn.classList.remove('flex');
}
async function loadPostHistory(postId) {
// Mark sidebar item as active
document.querySelectorAll('.post-sidebar-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.postId === postId) {
item.classList.add('active');
}
});
// Show loading in chat area
const container = document.getElementById('chat-messages');
container.innerHTML = `
<div class="flex items-center justify-center h-64">
<div class="text-gray-400">
<svg class="w-8 h-8 animate-spin mx-auto mb-2" 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>
<p>Lade Chat-Verlauf...</p>
</div>
</div>
`;
try {
const response = await fetch(`/api/employee/chat/history/${postId}`);
const result = await response.json();
if (!result.success) {
showToast('Fehler: ' + (result.error || 'Konnte Post nicht laden'), 'error');
return;
}
// Update state
currentLoadedPostId = postId;
chatHistory = result.chat_history;
currentPost = result.post;
selectedPostTypeId = result.post_type_id;
// Clear chat area
container.innerHTML = '';
// Add welcome message
const welcomeDiv = document.createElement('div');
welcomeDiv.className = 'flex items-start gap-3';
welcomeDiv.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">
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>
`;
container.appendChild(welcomeDiv);
// Rebuild chat from history
chatHistory.forEach(item => {
// Add user message if exists
if (item.user && item.user.trim()) {
addMessageToChat('user', item.user);
}
// Add AI message if exists
if (item.ai) {
addMessageToChat('ai', item.explanation || 'Hier ist dein Post:', item.ai);
}
});
// Update post type selection
if (selectedPostTypeId) {
updatePostTypeSelection(selectedPostTypeId);
}
// Scroll to bottom
setTimeout(() => {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}, 100);
} catch (error) {
console.error('Error loading post history:', error);
showToast('Netzwerkfehler beim Laden', 'error');
// Restore welcome message on error
container.innerHTML = `
<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>
`;
}
}
function updatePostTypeSelection(postTypeId) {
// Remove active state from all chips
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');
});
// Add active state to matching chip
const targetChip = document.querySelector(`.post-type-chip[data-post-type-id="${postTypeId}"]`);
if (targetChip) {
targetChip.classList.remove('bg-brand-bg-dark', 'text-gray-400');
targetChip.classList.add('bg-brand-highlight', 'text-black');
targetChip.setAttribute('data-selected', 'true');
}
}
async function autoSaveLoadedPost() {
if (!currentLoadedPostId || !currentPost) {
return;
}
try {
const response = await fetch(`/api/employee/chat/update/${currentLoadedPostId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
post_content: currentPost,
chat_history: chatHistory
})
});
const result = await response.json();
if (!result.success) {
showToast('Fehler beim Auto-Speichern: ' + (result.error || 'Unbekannter Fehler'), 'error');
}
// Silent success - no toast for successful auto-save
} catch (error) {
console.error('Error auto-saving post:', error);
showToast('Netzwerkfehler beim Auto-Speichern', 'error');
}
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
const colors = {

View File

@@ -1714,12 +1714,17 @@ async def chat_create_page(request: Request):
profile_picture = await get_user_avatar(session, user_id)
# Load all saved posts for sidebar (exclude scheduled and published)
all_posts = await db.get_generated_posts(user_id)
saved_posts = [post for post in all_posts if post.status not in ['scheduled', 'published']]
return templates.TemplateResponse("chat_create.html", {
"request": request,
"page": "chat-create",
"session": session,
"post_types": post_types,
"profile_picture": profile_picture
"profile_picture": profile_picture,
"saved_posts": saved_posts
})
@@ -3550,6 +3555,139 @@ async def chat_save_post(request: Request):
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@user_router.get("/api/employee/chat/history/{post_id}")
async def get_chat_history(request: Request, post_id: str):
"""Get chat history for a saved post."""
session = require_user_session(request)
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
user_id = UUID(session.user_id)
post_uuid = UUID(post_id)
# Fetch post
post = await db.get_generated_post(post_uuid)
if not post:
return JSONResponse({"success": False, "error": "Post nicht gefunden"}, status_code=404)
# Verify ownership
if post.user_id != user_id:
return JSONResponse({"success": False, "error": "Nicht autorisiert"}, status_code=403)
# Reconstruct chat history from writer_versions and critic_feedback
chat_history = []
# First version: AI generates initial post (no user message)
if post.writer_versions and len(post.writer_versions) > 0:
first_explanation = "Hier ist dein erster Entwurf:"
# Check if first critic feedback has explanation (from AI)
if post.critic_feedback and len(post.critic_feedback) > 0:
first_explanation = post.critic_feedback[0].get('explanation', first_explanation)
chat_history.append({
"user": "", # No user message for first generation
"ai": post.writer_versions[0],
"explanation": first_explanation
})
# Subsequent versions: User feedback → AI refined version
for i in range(1, len(post.writer_versions)):
user_message = ""
explanation = "Hier ist die überarbeitete Version:"
# Get user feedback from critic_feedback (offset by 1, since first is for initial)
if i <= len(post.critic_feedback):
feedback_item = post.critic_feedback[i - 1]
user_message = feedback_item.get('feedback', '')
explanation = feedback_item.get('explanation', explanation)
chat_history.append({
"user": user_message,
"ai": post.writer_versions[i],
"explanation": explanation
})
return JSONResponse({
"success": True,
"chat_history": chat_history,
"post": post.post_content,
"post_type_id": str(post.post_type_id) if post.post_type_id else None,
"topic_title": post.topic_title
})
except ValueError:
return JSONResponse({"success": False, "error": "Ungültige Post-ID"}, status_code=400)
except Exception as e:
logger.error(f"Error fetching chat history: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@user_router.put("/api/employee/chat/update/{post_id}")
async def update_chat_post(request: Request, post_id: str):
"""Update an existing post with new chat conversation."""
session = require_user_session(request)
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
user_id = UUID(session.user_id)
post_uuid = UUID(post_id)
data = await request.json()
post_content = data.get("post_content", "").strip()
chat_history = data.get("chat_history", [])
if not post_content:
return JSONResponse({"success": False, "error": "Post-Inhalt erforderlich"})
# Fetch existing post
post = await db.get_generated_post(post_uuid)
if not post:
return JSONResponse({"success": False, "error": "Post nicht gefunden"}, status_code=404)
# Verify ownership
if post.user_id != user_id:
return JSONResponse({"success": False, "error": "Nicht autorisiert"}, status_code=403)
# 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', '')
})
# Prepare update data
updates = {
'post_content': post_content,
'writer_versions': writer_versions,
'critic_feedback': critic_feedback_list,
'iterations': len(writer_versions)
}
# Update the post using the correct method
updated_post = await db.update_generated_post(post_uuid, updates)
return JSONResponse({
"success": True,
"post_id": str(updated_post.id),
"message": "Post erfolgreich aktualisiert"
})
except ValueError:
return JSONResponse({"success": False, "error": "Ungültige Post-ID"}, status_code=400)
except Exception as e:
logger.error(f"Error updating 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."""