diff --git a/maintenance_fix_markdown_bold.py b/maintenance_fix_markdown_bold.py
index 6818945..5d4be8c 100644
--- a/maintenance_fix_markdown_bold.py
+++ b/maintenance_fix_markdown_bold.py
@@ -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}")
diff --git a/src/web/templates/user/chat_create.html b/src/web/templates/user/chat_create.html
index 8fd5f88..44072e7 100644
--- a/src/web/templates/user/chat_create.html
+++ b/src/web/templates/user/chat_create.html
@@ -3,6 +3,32 @@
{% block title %}Chat Assistent{% endblock %}
{% block content %}
+
+
+
+
+
+
+
+ {% if saved_posts %}
+ {% for post in saved_posts %}
+
+
{{ post.topic_title }}
+ {{ post.status }}
+
+ {% endfor %}
+ {% else %}
+
+ Noch keine Posts vorhanden
+
+ {% endif %}
+
+
+
@@ -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 = '';
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 = `
+
+
+ 🤖
+
+
+
+
+ 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.
+
+
+ Du kannst mich danach bitten, den Post anzupassen, umzuschreiben oder zu verbessern.
+
+
+
+
+ `;
+
+ // 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 = `
+
+ 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.
+
+
+ Du kannst mich danach bitten, den Post anzupassen, umzuschreiben oder zu verbessern.
+
+
+
+ `;
+ 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 = `
+
+
+ 🤖
+
+
+
+
+ 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.
+
+
+ Du kannst mich danach bitten, den Post anzupassen, umzuschreiben oder zu verbessern.
+
+
+
+
+ `;
+ }
+}
+
+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 = {
diff --git a/src/web/user/routes.py b/src/web/user/routes.py
index 9058ecd..c95df00 100644
--- a/src/web/user/routes.py
+++ b/src/web/user/routes.py
@@ -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."""