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 %} + + {% 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 = ` +
+
+ + + +

Lade Chat-Verlauf...

+
+
+ `; + + 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 = ` +
+ 🤖 +
+
+
+

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